tweepy-self 1.11.0__tar.gz → 1.12.0__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/PKG-INFO +3 -3
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/pyproject.toml +3 -3
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/__init__.py +0 -5
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/account.py +1 -1
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/base/session.py +4 -3
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/client.py +67 -61
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/errors.py +8 -14
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/utils/__init__.py +2 -0
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/utils/other.py +4 -0
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/README.md +0 -0
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/_capsolver/__init__.py +0 -0
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/_capsolver/core/__init__.py +0 -0
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/_capsolver/core/base.py +0 -0
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/_capsolver/core/config.py +0 -0
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/_capsolver/core/enum.py +0 -0
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/_capsolver/core/serializer.py +0 -0
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/_capsolver/fun_captcha.py +0 -0
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/base/__init__.py +0 -0
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/base/client.py +0 -0
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/enums.py +0 -0
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/models.py +0 -0
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/utils/file.py +0 -0
- {tweepy_self-1.11.0 → tweepy_self-1.12.0}/twitter/utils/html.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: tweepy-self
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.12.0
|
4
4
|
Summary: Twitter (selfbot) for Python!
|
5
5
|
Home-page: https://github.com/alenkimov/tweepy-self
|
6
6
|
Author: Alen
|
@@ -12,13 +12,13 @@ Classifier: Programming Language :: Python :: 3.12
|
|
12
12
|
Requires-Dist: aiohttp (>=3.9,<4.0)
|
13
13
|
Requires-Dist: beautifulsoup4 (>=4,<5)
|
14
14
|
Requires-Dist: better-proxy (>=1.1,<2.0)
|
15
|
-
Requires-Dist: curl_cffi (==0.
|
15
|
+
Requires-Dist: curl_cffi (==0.9.0b2)
|
16
16
|
Requires-Dist: loguru (>=0.7,<0.8)
|
17
17
|
Requires-Dist: lxml (>=5,<6)
|
18
18
|
Requires-Dist: pydantic (>=2,<3)
|
19
19
|
Requires-Dist: pyotp (>=2,<3)
|
20
20
|
Requires-Dist: requests (>=2,<3)
|
21
|
-
Requires-Dist: tenacity (>=
|
21
|
+
Requires-Dist: tenacity (>=9,<10)
|
22
22
|
Requires-Dist: yarl (>=1,<2)
|
23
23
|
Project-URL: Repository, https://github.com/alenkimov/tweepy-self
|
24
24
|
Project-URL: Source, https://github.com/alenkimov/tweepy-self
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "tweepy-self"
|
3
|
-
version = "1.
|
3
|
+
version = "1.12.0"
|
4
4
|
description = "Twitter (selfbot) for Python!"
|
5
5
|
authors = ["Alen <alen.kimov@gmail.com>"]
|
6
6
|
readme = "README.md"
|
@@ -12,7 +12,7 @@ Source = "https://github.com/alenkimov/tweepy-self"
|
|
12
12
|
|
13
13
|
[tool.poetry.dependencies]
|
14
14
|
python = "^3.11"
|
15
|
-
curl_cffi = "0.
|
15
|
+
curl_cffi = "0.9.0b2"
|
16
16
|
better-proxy = "^1.1"
|
17
17
|
beautifulsoup4 = "^4"
|
18
18
|
pydantic = "^2"
|
@@ -20,7 +20,7 @@ lxml = "^5"
|
|
20
20
|
pyotp = "^2"
|
21
21
|
yarl = "^1"
|
22
22
|
aiohttp = "^3.9"
|
23
|
-
tenacity = "^
|
23
|
+
tenacity = "^9"
|
24
24
|
requests = "^2"
|
25
25
|
loguru = "^0.7"
|
26
26
|
|
@@ -14,13 +14,14 @@ class BaseAsyncSession(requests.AsyncSession):
|
|
14
14
|
DEFAULT_HEADERS = {
|
15
15
|
"accept": "*/*",
|
16
16
|
"accept-language": "en-US,en",
|
17
|
-
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
18
|
-
|
17
|
+
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
|
18
|
+
'Priority': 'u=1, i',
|
19
|
+
"sec-ch-ua": '"Google Chrome";v="125", "Chromium";v="125", "Not.A/Brand";v="24"',
|
19
20
|
"sec-ch-ua-platform": '"Windows"',
|
20
21
|
"sec-ch-ua-mobile": "?0",
|
21
22
|
"sec-fetch-dest": "empty",
|
22
23
|
"sec-fetch-mode": "cors",
|
23
|
-
"sec-fetch-site": "
|
24
|
+
"sec-fetch-site": "ssame-site",
|
24
25
|
"connection": "keep-alive",
|
25
26
|
}
|
26
27
|
DEFAULT_IMPERSONATE = requests.BrowserType.chrome120
|
@@ -31,27 +31,26 @@ from .errors import (
|
|
31
31
|
from .base import BaseHTTPClient
|
32
32
|
from .account import Account, AccountStatus
|
33
33
|
from .models import User, Tweet, Media, Subtask
|
34
|
-
from .utils import
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
)
|
34
|
+
from .utils import parse_oauth_html
|
35
|
+
from .utils import parse_unlock_html
|
36
|
+
from .utils import tweets_data_from_instructions
|
37
|
+
from .utils import encode_x_client_transaction_id
|
39
38
|
|
40
39
|
|
41
40
|
class Client(BaseHTTPClient):
|
42
41
|
_BEARER_TOKEN = "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"
|
43
42
|
_DEFAULT_HEADERS = {
|
44
|
-
"authority": "
|
45
|
-
"origin": "https://
|
43
|
+
"authority": "x.com",
|
44
|
+
"origin": "https://x.com",
|
46
45
|
"x-twitter-active-user": "yes",
|
47
46
|
"x-twitter-client-language": "en",
|
48
47
|
}
|
49
|
-
_GRAPHQL_URL = "https://
|
48
|
+
_GRAPHQL_URL = "https://x.com/i/api/graphql"
|
50
49
|
_ACTION_TO_QUERY_ID = {
|
51
50
|
"CreateRetweet": "ojPdsZsimiJrUGLR1sjUtA",
|
52
51
|
"FavoriteTweet": "lI07N6Otwv1PhnEgXILM7A",
|
53
52
|
"UnfavoriteTweet": "ZYKSe-w7KEslx3JhSIk5LA",
|
54
|
-
"CreateTweet": "
|
53
|
+
"CreateTweet": "oB-5XsHNAbjvARJEc8CZFw",
|
55
54
|
"TweetResultByRestId": "V3vfsYzNEyD9tsf4xoFRgw",
|
56
55
|
"ModerateTweet": "p'jF:GVqCjTcZol0xcBJjw",
|
57
56
|
"DeleteTweet": "VaenaVgh5q5ih7kvyVjgtg",
|
@@ -62,9 +61,9 @@ class Client(BaseHTTPClient):
|
|
62
61
|
"Followers": "3yX7xr2hKjcZYnXt6cU6lQ",
|
63
62
|
"UserByScreenName": "G3KGOASz96M-Qu0nwmGXNg",
|
64
63
|
"UsersByRestIds": "itEhGywpgX9b3GJCzOtSrA",
|
65
|
-
"Viewer": "
|
64
|
+
"Viewer": "-876iyxD1O_0X0BqeykjZA",
|
66
65
|
}
|
67
|
-
_CAPTCHA_URL = "https://
|
66
|
+
_CAPTCHA_URL = "https://x.com/account/access"
|
68
67
|
_CAPTCHA_SITE_KEY = "0152B4EB-D2DC-460A-89A1-629838B529C9"
|
69
68
|
|
70
69
|
@classmethod
|
@@ -103,16 +102,19 @@ class Client(BaseHTTPClient):
|
|
103
102
|
|
104
103
|
async def _request(
|
105
104
|
self,
|
106
|
-
method,
|
107
|
-
url,
|
105
|
+
method: str,
|
106
|
+
url: str | URL,
|
108
107
|
*,
|
109
108
|
auth: bool = True,
|
110
109
|
bearer: bool = True,
|
111
110
|
wait_on_rate_limit: bool = None,
|
112
111
|
**kwargs,
|
113
112
|
) -> tuple[requests.Response, Any]:
|
114
|
-
cookies = kwargs["cookies"] = kwargs.get("cookies"
|
115
|
-
headers = kwargs["headers"] = kwargs.get("headers"
|
113
|
+
cookies = kwargs["cookies"] = kwargs.get("cookies", {})
|
114
|
+
headers = kwargs["headers"] = kwargs.get("headers", {})
|
115
|
+
|
116
|
+
url = URL(url)
|
117
|
+
headers["x-client-transaction-id"] = encode_x_client_transaction_id(url.path)
|
116
118
|
|
117
119
|
if bearer:
|
118
120
|
headers["authorization"] = f"Bearer {self._BEARER_TOKEN}"
|
@@ -141,7 +143,7 @@ class Client(BaseHTTPClient):
|
|
141
143
|
# fmt: on
|
142
144
|
|
143
145
|
try:
|
144
|
-
response = await self._session.request(method, url, **kwargs)
|
146
|
+
response = await self._session.request(method, str(url), **kwargs)
|
145
147
|
except requests.errors.RequestsError as exc:
|
146
148
|
if exc.code == 35:
|
147
149
|
msg = (
|
@@ -159,7 +161,7 @@ class Client(BaseHTTPClient):
|
|
159
161
|
f"\nResponse data: {data}")
|
160
162
|
# fmt: on
|
161
163
|
|
162
|
-
if ct0 := self._session.cookies.get("ct0", domain=".
|
164
|
+
if ct0 := self._session.cookies.get("ct0", domain=".x.com"):
|
163
165
|
self.account.ct0 = ct0
|
164
166
|
|
165
167
|
auth_token = self._session.cookies.get("auth_token")
|
@@ -179,12 +181,12 @@ class Client(BaseHTTPClient):
|
|
179
181
|
if isinstance(data, dict) and "errors" in data:
|
180
182
|
exc = HTTPException(response, data)
|
181
183
|
|
182
|
-
if 141 in exc.
|
184
|
+
if 141 in exc.error_codes or 37 in exc.error_codes:
|
183
185
|
self.account.status = AccountStatus.SUSPENDED
|
184
186
|
raise AccountSuspended(exc, self.account)
|
185
187
|
|
186
|
-
if 326 in exc.
|
187
|
-
for error_data in exc.
|
188
|
+
if 326 in exc.error_codes:
|
189
|
+
for error_data in exc.errors:
|
188
190
|
if (
|
189
191
|
error_data.get("code") == 326
|
190
192
|
and error_data.get("bounce_location")
|
@@ -202,7 +204,7 @@ class Client(BaseHTTPClient):
|
|
202
204
|
if response.status_code == 400:
|
203
205
|
exc = BadRequest(response, data)
|
204
206
|
|
205
|
-
if 399 in exc.
|
207
|
+
if 399 in exc.error_codes:
|
206
208
|
self.account.status = AccountStatus.NOT_FOUND
|
207
209
|
raise AccountNotFound(exc, self.account)
|
208
210
|
|
@@ -211,7 +213,7 @@ class Client(BaseHTTPClient):
|
|
211
213
|
if response.status_code == 401:
|
212
214
|
exc = Unauthorized(response, data)
|
213
215
|
|
214
|
-
if 32 in exc.
|
216
|
+
if 32 in exc.error_codes:
|
215
217
|
self.account.status = AccountStatus.BAD_TOKEN
|
216
218
|
raise BadAccountToken(exc, self.account)
|
217
219
|
|
@@ -220,12 +222,12 @@ class Client(BaseHTTPClient):
|
|
220
222
|
if response.status_code == 403:
|
221
223
|
exc = Forbidden(response, data)
|
222
224
|
|
223
|
-
if 64 in exc.
|
225
|
+
if 64 in exc.error_codes:
|
224
226
|
self.account.status = AccountStatus.SUSPENDED
|
225
227
|
raise AccountSuspended(exc, self.account)
|
226
228
|
|
227
|
-
if 326 in exc.
|
228
|
-
for error_data in exc.
|
229
|
+
if 326 in exc.error_codes:
|
230
|
+
for error_data in exc.errors:
|
229
231
|
if (
|
230
232
|
error_data.get("code") == 326
|
231
233
|
and error_data.get("bounce_location") == "/i/flow/consent_flow"
|
@@ -269,8 +271,8 @@ class Client(BaseHTTPClient):
|
|
269
271
|
|
270
272
|
async def request(
|
271
273
|
self,
|
272
|
-
method,
|
273
|
-
url,
|
274
|
+
method: str,
|
275
|
+
url: str | URL,
|
274
276
|
*,
|
275
277
|
auto_unlock: bool = True,
|
276
278
|
auto_relogin: bool = None,
|
@@ -303,7 +305,7 @@ class Client(BaseHTTPClient):
|
|
303
305
|
except Forbidden as exc:
|
304
306
|
if (
|
305
307
|
rerequest_on_bad_ct0
|
306
|
-
and 353 in exc.
|
308
|
+
and 353 in exc.error_codes
|
307
309
|
and "ct0" in exc.response.cookies
|
308
310
|
):
|
309
311
|
return await self.request(
|
@@ -327,7 +329,7 @@ class Client(BaseHTTPClient):
|
|
327
329
|
scope: str,
|
328
330
|
response_type: str,
|
329
331
|
) -> str:
|
330
|
-
url = "https://
|
332
|
+
url = "https://x.com/i/api/2/oauth2/authorize"
|
331
333
|
querystring = {
|
332
334
|
"client_id": client_id,
|
333
335
|
"code_challenge": code_challenge,
|
@@ -349,7 +351,7 @@ class Client(BaseHTTPClient):
|
|
349
351
|
headers = {"content-type": "application/x-www-form-urlencoded"}
|
350
352
|
await self.request(
|
351
353
|
"POST",
|
352
|
-
"https://
|
354
|
+
"https://x.com/i/api/2/oauth2/authorize",
|
353
355
|
headers=headers,
|
354
356
|
data=data,
|
355
357
|
)
|
@@ -393,7 +395,7 @@ class Client(BaseHTTPClient):
|
|
393
395
|
|
394
396
|
:return: Response: html страница привязки приложения (аутентификации) старого типа.
|
395
397
|
"""
|
396
|
-
url = "https://api.
|
398
|
+
url = "https://api.x.com/oauth/authenticate"
|
397
399
|
oauth_params["oauth_token"] = oauth_token
|
398
400
|
response, _ = await self.request("GET", url, params=oauth_params)
|
399
401
|
|
@@ -411,7 +413,7 @@ class Client(BaseHTTPClient):
|
|
411
413
|
authenticity_token: str,
|
412
414
|
redirect_after_login_url: str,
|
413
415
|
) -> requests.Response:
|
414
|
-
url = "https://api.
|
416
|
+
url = "https://api.x.com/oauth/authorize"
|
415
417
|
params = {
|
416
418
|
"redirect_after_login": redirect_after_login_url,
|
417
419
|
"authenticity_token": authenticity_token,
|
@@ -441,7 +443,7 @@ class Client(BaseHTTPClient):
|
|
441
443
|
return authenticity_token, redirect_url
|
442
444
|
|
443
445
|
async def _update_account_username(self):
|
444
|
-
url = "https://
|
446
|
+
url = "https://api.x.com/1.1/account/settings.json"
|
445
447
|
response, response_json = await self.request("POST", url)
|
446
448
|
self.account.username = response_json["screen_name"]
|
447
449
|
|
@@ -556,7 +558,7 @@ class Client(BaseHTTPClient):
|
|
556
558
|
|
557
559
|
:return: Media
|
558
560
|
"""
|
559
|
-
url = "https://upload.
|
561
|
+
url = "https://upload.x.com/1.1/media/upload.json"
|
560
562
|
payload = {"media_data": base64.b64encode(image)}
|
561
563
|
for attempt in range(attempts):
|
562
564
|
try:
|
@@ -581,7 +583,7 @@ class Client(BaseHTTPClient):
|
|
581
583
|
raise
|
582
584
|
|
583
585
|
async def _follow_action(self, action: str, user_id: int | str) -> bool:
|
584
|
-
url = f"https://
|
586
|
+
url = f"https://x.com/i/api/1.1/friendships/{action}.json"
|
585
587
|
params = {
|
586
588
|
"include_profile_interstitial_type": "1",
|
587
589
|
"include_blocking": "1",
|
@@ -638,7 +640,7 @@ class Client(BaseHTTPClient):
|
|
638
640
|
if (
|
639
641
|
search_duplicate
|
640
642
|
and 327
|
641
|
-
in exc.
|
643
|
+
in exc.error_codes # duplicate retweet (You have already retweeted this Tweet)
|
642
644
|
):
|
643
645
|
tweets = await self.request_tweets(self.account.id)
|
644
646
|
duplicate_tweet = None
|
@@ -682,7 +684,7 @@ class Client(BaseHTTPClient):
|
|
682
684
|
try:
|
683
685
|
response_json = await self._interact_with_tweet("FavoriteTweet", tweet_id)
|
684
686
|
except HTTPException as exc:
|
685
|
-
if 139 in exc.
|
687
|
+
if 139 in exc.error_codes:
|
686
688
|
# Already liked
|
687
689
|
return True
|
688
690
|
else:
|
@@ -711,7 +713,7 @@ class Client(BaseHTTPClient):
|
|
711
713
|
return is_deleted
|
712
714
|
|
713
715
|
async def pin_tweet(self, tweet_id: str | int) -> bool:
|
714
|
-
url = "https://api.
|
716
|
+
url = "https://api.x.com/1.1/account/pin_tweet.json"
|
715
717
|
data = {
|
716
718
|
"tweet_mode": "extended",
|
717
719
|
"id": str(tweet_id),
|
@@ -761,9 +763,12 @@ class Client(BaseHTTPClient):
|
|
761
763
|
"longform_notetweets_consumption_enabled": True,
|
762
764
|
"responsive_web_twitter_article_tweet_consumption_enabled": True,
|
763
765
|
"tweet_awards_web_tipping_enabled": False,
|
766
|
+
"creator_subscriptions_quote_tweet_preview_enabled": False,
|
764
767
|
"longform_notetweets_rich_text_read_enabled": True,
|
765
768
|
"longform_notetweets_inline_media_enabled": True,
|
769
|
+
"articles_preview_enabled": True,
|
766
770
|
"rweb_video_timestamps_enabled": True,
|
771
|
+
"rweb_tipjar_consumption_enabled": True,
|
767
772
|
"responsive_web_graphql_exclude_directive_enabled": True,
|
768
773
|
"verified_phone_label_enabled": False,
|
769
774
|
"freedom_of_speech_not_reach_fetch_enabled": True,
|
@@ -803,7 +808,7 @@ class Client(BaseHTTPClient):
|
|
803
808
|
except HTTPException as exc:
|
804
809
|
if (
|
805
810
|
search_duplicate
|
806
|
-
and 187 in exc.
|
811
|
+
and 187 in exc.error_codes # duplicate tweet (Status is a duplicate)
|
807
812
|
):
|
808
813
|
tweets = await self.request_tweets()
|
809
814
|
duplicate_tweet = None
|
@@ -886,7 +891,7 @@ class Client(BaseHTTPClient):
|
|
886
891
|
"""
|
887
892
|
:return: Raw vote information
|
888
893
|
"""
|
889
|
-
url = "https://caps.
|
894
|
+
url = "https://caps.x.com/v2/capi/passthrough/1"
|
890
895
|
params = {
|
891
896
|
"twitter:string:card_uri": f"card://{card_id}",
|
892
897
|
"twitter:long:original_tweet_id": str(tweet_id),
|
@@ -1101,7 +1106,7 @@ class Client(BaseHTTPClient):
|
|
1101
1106
|
"""
|
1102
1107
|
:return: Image URL
|
1103
1108
|
"""
|
1104
|
-
url = f"https://api.
|
1109
|
+
url = f"https://api.x.com/1.1/account/update_profile_{type}.json"
|
1105
1110
|
params = {
|
1106
1111
|
"media_id": str(media_id),
|
1107
1112
|
"include_profile_interstitial_type": "1",
|
@@ -1136,7 +1141,7 @@ class Client(BaseHTTPClient):
|
|
1136
1141
|
return await self._update_profile_image("banner", media_id)
|
1137
1142
|
|
1138
1143
|
async def change_username(self, username: str) -> bool:
|
1139
|
-
url = "https://
|
1144
|
+
url = "https://x.com/i/api/1.1/account/settings.json"
|
1140
1145
|
payload = {"screen_name": username}
|
1141
1146
|
response, data = await self.request("POST", url, data=payload)
|
1142
1147
|
new_username = data["screen_name"]
|
@@ -1151,7 +1156,7 @@ class Client(BaseHTTPClient):
|
|
1151
1156
|
if not self.account.password:
|
1152
1157
|
raise ValueError(f"Specify the current password before changing it")
|
1153
1158
|
|
1154
|
-
url = "https://
|
1159
|
+
url = "https://x.com/i/api/i/account/change_password.json"
|
1155
1160
|
payload = {
|
1156
1161
|
"current_password": self.account.password,
|
1157
1162
|
"password": password,
|
@@ -1175,7 +1180,7 @@ class Client(BaseHTTPClient):
|
|
1175
1180
|
if name is None and description is None:
|
1176
1181
|
raise ValueError("Specify at least one param")
|
1177
1182
|
|
1178
|
-
url = "https://
|
1183
|
+
url = "https://x.com/i/api/1.1/account/update_profile.json"
|
1179
1184
|
# Создаем словарь data, включая в него только те ключи, для которых значения не равны None
|
1180
1185
|
payload = {
|
1181
1186
|
k: v
|
@@ -1200,9 +1205,9 @@ class Client(BaseHTTPClient):
|
|
1200
1205
|
return updated
|
1201
1206
|
|
1202
1207
|
async def establish_status(self):
|
1203
|
-
url = "https://
|
1208
|
+
url = "https://api.x.com/1.1/account/personalization/p13n_preferences.json"
|
1204
1209
|
try:
|
1205
|
-
await self.request("
|
1210
|
+
await self.request("GET", url, auto_unlock=False, auto_relogin=False)
|
1206
1211
|
self.account.status = AccountStatus.GOOD
|
1207
1212
|
except BadAccount:
|
1208
1213
|
pass
|
@@ -1215,7 +1220,7 @@ class Client(BaseHTTPClient):
|
|
1215
1220
|
visibility: Literal["self", "mutualfollow"] = "self",
|
1216
1221
|
year_visibility: Literal["self"] = "self",
|
1217
1222
|
) -> bool:
|
1218
|
-
url = "https://
|
1223
|
+
url = "https://x.com/i/api/1.1/account/update_profile.json"
|
1219
1224
|
payload = {
|
1220
1225
|
"birthdate_day": day,
|
1221
1226
|
"birthdate_month": month,
|
@@ -1240,7 +1245,7 @@ class Client(BaseHTTPClient):
|
|
1240
1245
|
"""
|
1241
1246
|
:return: Event data
|
1242
1247
|
"""
|
1243
|
-
url = "https://api.
|
1248
|
+
url = "https://api.x.com/1.1/direct_messages/events/new.json"
|
1244
1249
|
payload = {
|
1245
1250
|
"event": {
|
1246
1251
|
"type": "message_create",
|
@@ -1262,7 +1267,7 @@ class Client(BaseHTTPClient):
|
|
1262
1267
|
|
1263
1268
|
:return: Event data
|
1264
1269
|
"""
|
1265
|
-
url = f"https://api.
|
1270
|
+
url = f"https://api.x.com/2/dm_conversations/{conversation_id}/messages"
|
1266
1271
|
payload = {"text": text}
|
1267
1272
|
response, response_json = await self.request("POST", url, json=payload)
|
1268
1273
|
event_data = response_json["event"]
|
@@ -1272,7 +1277,7 @@ class Client(BaseHTTPClient):
|
|
1272
1277
|
"""
|
1273
1278
|
:return: Messages data
|
1274
1279
|
"""
|
1275
|
-
url = "https://
|
1280
|
+
url = "https://x.com/i/api/1.1/dm/inbox_initial_state.json"
|
1276
1281
|
params = {
|
1277
1282
|
"nsfw_filtering_enabled": "false",
|
1278
1283
|
"filter_low_quality": "false",
|
@@ -1414,7 +1419,7 @@ class Client(BaseHTTPClient):
|
|
1414
1419
|
verification_string=token,
|
1415
1420
|
)
|
1416
1421
|
|
1417
|
-
if response.url == "https://
|
1422
|
+
if response.url == "https://x.com/?lang=en":
|
1418
1423
|
break
|
1419
1424
|
|
1420
1425
|
(
|
@@ -1444,7 +1449,7 @@ class Client(BaseHTTPClient):
|
|
1444
1449
|
await self.establish_status()
|
1445
1450
|
|
1446
1451
|
async def update_backup_code(self):
|
1447
|
-
url = "https://api.
|
1452
|
+
url = "https://api.x.com/1.1/account/backup_code.json"
|
1448
1453
|
response, response_json = await self.request("GET", url)
|
1449
1454
|
self.account.backup_code = response_json["codes"][0]
|
1450
1455
|
|
@@ -1452,7 +1457,7 @@ class Client(BaseHTTPClient):
|
|
1452
1457
|
"""
|
1453
1458
|
:return: flow_token and subtasks
|
1454
1459
|
"""
|
1455
|
-
url = "https://api.
|
1460
|
+
url = "https://api.x.com/1.1/onboarding/task.json"
|
1456
1461
|
response, data = await self.request("POST", url, **request_kwargs)
|
1457
1462
|
subtasks = [
|
1458
1463
|
Subtask.from_raw_data(subtask_data) for subtask_data in data["subtasks"]
|
@@ -1629,6 +1634,7 @@ class Client(BaseHTTPClient):
|
|
1629
1634
|
async def _viewer(self):
|
1630
1635
|
url, query_id = self._action_to_url("Viewer")
|
1631
1636
|
features = {
|
1637
|
+
"rweb_tipjar_consumption_enabled": True,
|
1632
1638
|
"responsive_web_graphql_exclude_directive_enabled": True,
|
1633
1639
|
"verified_phone_label_enabled": False,
|
1634
1640
|
"creator_subscriptions_tweet_preview_api_enabled": True,
|
@@ -1655,7 +1661,7 @@ class Client(BaseHTTPClient):
|
|
1655
1661
|
"""
|
1656
1662
|
response, data = await self._request(
|
1657
1663
|
"POST",
|
1658
|
-
"https://api.
|
1664
|
+
"https://api.x.com/1.1/guest/activate.json",
|
1659
1665
|
auth=False,
|
1660
1666
|
)
|
1661
1667
|
return data["guest_token"]
|
@@ -1697,7 +1703,7 @@ class Client(BaseHTTPClient):
|
|
1697
1703
|
flow_token, subtasks = await self._login_acid(flow_token, self.account.email)
|
1698
1704
|
# fmt: on
|
1699
1705
|
except HTTPException as exc:
|
1700
|
-
if 399 in exc.
|
1706
|
+
if 399 in exc.error_codes:
|
1701
1707
|
logger.warning(
|
1702
1708
|
f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
|
1703
1709
|
f" Bad email!"
|
@@ -1721,7 +1727,7 @@ class Client(BaseHTTPClient):
|
|
1721
1727
|
flow_token, subtasks = await self._login_two_factor_auth_challenge(flow_token, self.account.get_totp_code())
|
1722
1728
|
# fmt: on
|
1723
1729
|
except HTTPException as exc:
|
1724
|
-
if 399 in exc.
|
1730
|
+
if 399 in exc.error_codes:
|
1725
1731
|
logger.warning(
|
1726
1732
|
f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
|
1727
1733
|
f" Bad TOTP secret!"
|
@@ -1737,7 +1743,7 @@ class Client(BaseHTTPClient):
|
|
1737
1743
|
try:
|
1738
1744
|
flow_token, subtasks = await self._login_two_factor_auth_challenge(flow_token, self.account.backup_code)
|
1739
1745
|
except HTTPException as exc:
|
1740
|
-
if 399 in exc.
|
1746
|
+
if 399 in exc.error_codes:
|
1741
1747
|
logger.warning(
|
1742
1748
|
f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
|
1743
1749
|
f" Bad backup code!"
|
@@ -1753,6 +1759,7 @@ class Client(BaseHTTPClient):
|
|
1753
1759
|
else:
|
1754
1760
|
raise
|
1755
1761
|
|
1762
|
+
await self._viewer()
|
1756
1763
|
await self._complete_subtask(flow_token, [])
|
1757
1764
|
return update_backup_code
|
1758
1765
|
|
@@ -1770,7 +1777,6 @@ class Client(BaseHTTPClient):
|
|
1770
1777
|
raise ValueError("No password")
|
1771
1778
|
|
1772
1779
|
update_backup_code = await self._login()
|
1773
|
-
await self._viewer()
|
1774
1780
|
|
1775
1781
|
if update_backup_code:
|
1776
1782
|
await self.update_backup_code()
|
@@ -1794,7 +1800,7 @@ class Client(BaseHTTPClient):
|
|
1794
1800
|
if not self.account.id:
|
1795
1801
|
await self.update_account_info()
|
1796
1802
|
|
1797
|
-
url = f"https://
|
1803
|
+
url = f"https://x.com/i/api/1.1/strato/column/User/{self.account.id}/account-security/twoFactorAuthSettings2"
|
1798
1804
|
response, data = await self.request("GET", url)
|
1799
1805
|
# fmt: off
|
1800
1806
|
return "Totp" in [method_data["twoFactorType"] for method_data in data["methods"]]
|
@@ -1949,7 +1955,7 @@ class Client(BaseHTTPClient):
|
|
1949
1955
|
|
1950
1956
|
|
1951
1957
|
class GQLClient:
|
1952
|
-
_GRAPHQL_URL = "https://
|
1958
|
+
_GRAPHQL_URL = "https://x.com/i/api/graphql"
|
1953
1959
|
_OPERATION_TO_QUERY_ID = {
|
1954
1960
|
"CreateRetweet": "ojPdsZsimiJrUGLR1sjUtA",
|
1955
1961
|
"FavoriteTweet": "lI07N6Otwv1PhnEgXILM7A",
|
@@ -1965,7 +1971,7 @@ class GQLClient:
|
|
1965
1971
|
"Followers": "3yX7xr2hKjcZYnXt6cU6lQ",
|
1966
1972
|
"UserByScreenName": "G3KGOASz96M-Qu0nwmGXNg",
|
1967
1973
|
"UsersByRestIds": "itEhGywpgX9b3GJCzOtSrA",
|
1968
|
-
"Viewer": "
|
1974
|
+
"Viewer": "-876iyxD1O_0X0BqeykjZA",
|
1969
1975
|
}
|
1970
1976
|
_DEFAULT_VARIABLES = {
|
1971
1977
|
"count": 1000,
|
@@ -41,10 +41,7 @@ def _http_exception_message(
|
|
41
41
|
if detail:
|
42
42
|
exception_message += f"\n(detail) {detail}"
|
43
43
|
for error in api_errors:
|
44
|
-
|
45
|
-
exception_message += f"\n(code {error['code']}) {error['message']}"
|
46
|
-
elif "message" in error:
|
47
|
-
exception_message += f"\n{error['message']}"
|
44
|
+
exception_message += f"\n{error}"
|
48
45
|
return exception_message
|
49
46
|
|
50
47
|
|
@@ -58,9 +55,8 @@ class HTTPException(TwitterException):
|
|
58
55
|
custom_exception_message: str = None,
|
59
56
|
):
|
60
57
|
self.response = response
|
61
|
-
self.
|
62
|
-
self.
|
63
|
-
self.api_messages: list[str] = []
|
58
|
+
self.errors: list[dict] = []
|
59
|
+
self.error_codes: list[int] = []
|
64
60
|
self.detail: str | None = None
|
65
61
|
self.html: str | None = None
|
66
62
|
|
@@ -81,17 +77,15 @@ class HTTPException(TwitterException):
|
|
81
77
|
super().__init__(exception_message)
|
82
78
|
return
|
83
79
|
|
84
|
-
self.
|
80
|
+
self.errors = data.get("errors", [data])
|
85
81
|
self.detail = data.get("detail")
|
86
82
|
|
87
|
-
for error in self.
|
83
|
+
for error in self.errors:
|
88
84
|
if "code" in error:
|
89
|
-
self.
|
90
|
-
if "message" in error:
|
91
|
-
self.api_messages.append(error["message"])
|
85
|
+
self.error_codes.append(error["code"])
|
92
86
|
|
93
87
|
exception_message = _http_exception_message(
|
94
|
-
response, self.
|
88
|
+
response, self.errors, self.detail, custom_exception_message
|
95
89
|
)
|
96
90
|
super().__init__(exception_message)
|
97
91
|
|
@@ -143,7 +137,7 @@ class BadAccount(TwitterException):
|
|
143
137
|
self.account = account
|
144
138
|
exception_message = _http_exception_message(
|
145
139
|
http_exception.response,
|
146
|
-
http_exception.
|
140
|
+
http_exception.errors,
|
147
141
|
http_exception.detail,
|
148
142
|
custom_exception_message or "Bad Twitter account.",
|
149
143
|
)
|
@@ -17,6 +17,7 @@ from .other import (
|
|
17
17
|
to_datetime,
|
18
18
|
hidden_value,
|
19
19
|
tweets_data_from_instructions,
|
20
|
+
encode_x_client_transaction_id,
|
20
21
|
)
|
21
22
|
|
22
23
|
|
@@ -35,4 +36,5 @@ __all__ = [
|
|
35
36
|
"to_datetime",
|
36
37
|
"hidden_value",
|
37
38
|
"tweets_data_from_instructions",
|
39
|
+
"encode_x_client_transaction_id",
|
38
40
|
]
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|