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.
- {tweepy_self-1.4.0.dist-info → tweepy_self-1.5.1.dist-info}/METADATA +2 -2
- tweepy_self-1.5.1.dist-info/RECORD +15 -0
- twitter/base/session.py +7 -4
- twitter/client.py +591 -374
- twitter/errors.py +45 -35
- twitter/models.py +29 -15
- twitter/utils/file.py +1 -1
- twitter/utils/html.py +26 -10
- twitter/utils/other.py +1 -1
- tweepy_self-1.4.0.dist-info/RECORD +0 -15
- {tweepy_self-1.4.0.dist-info → tweepy_self-1.5.1.dist-info}/WHEEL +0 -0
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 =
|
35
|
+
_BEARER_TOKEN = "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"
|
36
36
|
_DEFAULT_HEADERS = {
|
37
|
-
|
38
|
-
|
39
|
-
|
37
|
+
"authority": "twitter.com",
|
38
|
+
"origin": "https://twitter.com",
|
39
|
+
"x-twitter-active-user": "yes",
|
40
40
|
# 'x-twitter-auth-type': 'OAuth2Session',
|
41
|
-
|
41
|
+
"x-twitter-client-language": "en",
|
42
42
|
}
|
43
|
-
_GRAPHQL_URL =
|
43
|
+
_GRAPHQL_URL = "https://twitter.com/i/api/graphql"
|
44
44
|
_ACTION_TO_QUERY_ID = {
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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 =
|
61
|
-
_CAPTCHA_SITE_KEY =
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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 =
|
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[
|
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 (
|
157
|
-
|
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 (
|
189
|
-
|
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
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
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
|
-
|
232
|
-
|
238
|
+
"approval": "true",
|
239
|
+
"code": auth_code,
|
233
240
|
}
|
234
|
-
headers = {
|
235
|
-
await self.request(
|
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
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
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,
|
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(
|
278
|
-
|
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
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
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(
|
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(
|
307
|
-
|
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
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
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
|
-
|
418
|
+
"content-type": "application/x-www-form-urlencoded",
|
393
419
|
}
|
394
|
-
response, response_json = await self.request(
|
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
|
-
|
407
|
-
|
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(
|
422
|
-
retweet_id = int(
|
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(
|
427
|
-
is_liked = response_json[
|
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(
|
432
|
-
is_unliked =
|
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(
|
468
|
+
url, query_id = self._action_to_url("DeleteTweet")
|
437
469
|
json_payload = {
|
438
|
-
|
439
|
-
|
440
|
-
|
470
|
+
"variables": {
|
471
|
+
"tweet_id": tweet_id,
|
472
|
+
"dark_request": False,
|
441
473
|
},
|
442
|
-
|
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 =
|
481
|
+
url = "https://api.twitter.com/1.1/account/pin_tweet.json"
|
450
482
|
data = {
|
451
|
-
|
452
|
-
|
483
|
+
"tweet_mode": "extended",
|
484
|
+
"id": str(tweet_id),
|
453
485
|
}
|
454
486
|
headers = {
|
455
|
-
|
487
|
+
"content-type": "application/x-www-form-urlencoded",
|
456
488
|
}
|
457
|
-
response, response_json = await self.request(
|
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
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
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(
|
503
|
+
url, query_id = self._action_to_url("CreateTweet")
|
470
504
|
payload = {
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
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
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
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
|
-
|
531
|
+
"queryId": query_id,
|
500
532
|
}
|
501
533
|
if attachment_url:
|
502
|
-
payload[
|
534
|
+
payload["variables"]["attachment_url"] = attachment_url
|
503
535
|
if tweet_id_to_reply:
|
504
|
-
payload[
|
505
|
-
|
506
|
-
|
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[
|
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[
|
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(
|
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(
|
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(
|
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(
|
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
|
-
|
552
|
-
|
553
|
-
|
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
|
-
|
579
|
-
|
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
|
585
|
-
entries = response_json[
|
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[
|
588
|
-
user_data_dict = entry["content"]["itemContent"]["user_results"][
|
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(
|
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(
|
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(
|
652
|
+
return await self._request_users("Followers", self.account.id, count)
|
603
653
|
|
604
|
-
async def request_followings(
|
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(
|
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(
|
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 =
|
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
|
-
|
651
|
-
|
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(
|
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
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
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
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
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 = {
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
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(
|
748
|
-
|
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
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
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(
|
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
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
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 = {
|
793
|
-
"
|
794
|
-
|
795
|
-
"
|
796
|
-
"recipient_id": user_id
|
797
|
-
|
798
|
-
|
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 =
|
880
|
+
url = "https://twitter.com/i/api/1.1/dm/inbox_initial_state.json"
|
810
881
|
params = {
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
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 = [
|
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[
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
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
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
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
|
-
|
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
|
925
|
-
|
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(
|
946
|
-
|
947
|
-
|
948
|
-
|
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
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
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 =
|
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,
|
983
|
-
"
|
984
|
-
"
|
985
|
-
"
|
986
|
-
"
|
987
|
-
"
|
988
|
-
"
|
989
|
-
"
|
990
|
-
"
|
991
|
-
"
|
992
|
-
"
|
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
|
-
|
1001
|
-
|
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": {
|
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(
|
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": {
|
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
|
-
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
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
|
-
|
1075
|
-
|
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 =
|
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
|
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(
|
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
|
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
|
1146
|
-
response,
|
1147
|
-
return
|
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": {
|
1158
|
-
|
1159
|
-
|
1160
|
-
|
1161
|
-
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1165
|
-
|
1166
|
-
|
1167
|
-
|
1168
|
-
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
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": {
|
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(
|
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(
|
1388
|
+
async def _two_factor_enrollment_authentication_app_plain_code_subtask(
|
1389
|
+
self, flow_token: str
|
1390
|
+
):
|
1195
1391
|
subtask_inputs = [
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
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
|
-
|
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
|
-
|
1211
|
-
|
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
|
1415
|
+
async def _enable_totp(self):
|
1217
1416
|
flow_token, subtasks = await self._request_2fa_tasks()
|
1218
|
-
flow_token, subtasks =
|
1219
|
-
|
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
|
1223
|
-
|
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 =
|
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
|
1230
|
-
|
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
|
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.
|
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.
|
1463
|
+
await self._enable_totp()
|