tweepy-self 1.4.0__py3-none-any.whl → 1.5.1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- {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()
|