tweepy-self 1.10.0b8__tar.gz → 1.11.0__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/PKG-INFO +21 -7
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/README.md +20 -6
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/pyproject.toml +1 -1
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/__init__.py +1 -1
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/client.py +227 -53
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/enums.py +1 -0
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/errors.py +15 -8
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/models.py +12 -0
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/_capsolver/__init__.py +0 -0
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/_capsolver/core/__init__.py +0 -0
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/_capsolver/core/base.py +0 -0
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/_capsolver/core/config.py +0 -0
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/_capsolver/core/enum.py +0 -0
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/_capsolver/core/serializer.py +0 -0
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/_capsolver/fun_captcha.py +0 -0
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/account.py +0 -0
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/base/__init__.py +0 -0
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/base/client.py +0 -0
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/base/session.py +0 -0
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/utils/__init__.py +0 -0
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/utils/file.py +0 -0
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/utils/html.py +0 -0
- {tweepy_self-1.10.0b8 → tweepy_self-1.11.0}/twitter/utils/other.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: tweepy-self
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.11.0
|
4
4
|
Summary: Twitter (selfbot) for Python!
|
5
5
|
Home-page: https://github.com/alenkimov/tweepy-self
|
6
6
|
Author: Alen
|
@@ -32,12 +32,19 @@ Description-Content-Type: text/markdown
|
|
32
32
|
|
33
33
|
A modern, easy to use, feature-rich, and async ready API wrapper for Twitter's user API written in Python.
|
34
34
|
|
35
|
+
_NEW!_ Менеджер аккаунтов с базой данных:
|
36
|
+
- [tweepy-manager](https://github.com/alenkimov/tweepy-manager)
|
37
|
+
|
35
38
|
More libraries of the family:
|
36
39
|
- [better-proxy](https://github.com/alenkimov/better_proxy)
|
37
40
|
- [better-web3](https://github.com/alenkimov/better_web3)
|
38
41
|
|
39
42
|
Отдельное спасибо [Кузнице Ботов](https://t.me/bots_forge) за код для авторизации и разморозки! Подписывайтесь на их Telegram :)
|
40
43
|
|
44
|
+
Похожие библиотеки:
|
45
|
+
- [twikit (sync and async)](https://github.com/d60/twikit)
|
46
|
+
- [twitter-api-client (sync)](https://github.com/trevorhobenshield/twitter-api-client)
|
47
|
+
|
41
48
|
## Key Features
|
42
49
|
- Modern Pythonic API using async and await.
|
43
50
|
- Prevents user account automation detection.
|
@@ -84,7 +91,9 @@ logger.enable("twitter")
|
|
84
91
|
`level="DEBUG"` позволяет увидеть информацию обо всех запросах.
|
85
92
|
|
86
93
|
### Аккаунт
|
87
|
-
|
94
|
+
`twitter.Account`
|
95
|
+
|
96
|
+
#### Статусы аккаунта
|
88
97
|
- `UNKNOWN` - Статус аккаунта не установлен. Это статус по умолчанию.
|
89
98
|
- `BAD_TOKEN` - Неверный или мертвый токен.
|
90
99
|
- `SUSPENDED` - Действие учетной записи приостановлено. Тем не менее возможен запрос данных, а также авторизация через OAuth и OAuth2.
|
@@ -92,18 +101,23 @@ logger.enable("twitter")
|
|
92
101
|
- `CONSENT_LOCKED` - Учетная запись заморожена (лок). Условия для разморозки неизвестны.
|
93
102
|
- `GOOD` - Аккаунт в порядке.
|
94
103
|
|
104
|
+
Метод `Client.establish_status()` устанавливает статус аккаунта.
|
105
|
+
Также статус аккаунта может изменить любое взаимодействие с Twitter.
|
106
|
+
Поэтому, во время работы может внезапно быть вызвано исключение семейства `twitter.errors.BadAccount`.
|
107
|
+
|
95
108
|
Не каждое взаимодействие с Twitter достоверно определяет статус аккаунта.
|
96
|
-
Например, простой запрос данных об аккаунте честно вернет
|
109
|
+
Например, простой запрос данных об аккаунте честно вернет данные и не изменит статус, даже если действие вашей учетной записи приостановлено (`SUSPENDED`).
|
97
110
|
|
98
|
-
|
111
|
+
### Клиент
|
112
|
+
`twitter.Client`
|
99
113
|
|
100
|
-
|
101
|
-
|
114
|
+
#### Настройка
|
115
|
+
Клиент может быть сконфигурирован перед работой. Он принимает в себя следующие параметры:
|
102
116
|
- `wait_on_rate_limit` Если включено, то при достижении Rate Limit будет ждать, вместо того, чтобы выбрасывать исключение. Включено по умолчанию.
|
103
117
|
- `capsolver_api_key` API ключ сервиса [CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=m-aE3NeBGZLU). Нужен для автоматической разморозки аккаунта.
|
104
118
|
- `max_unlock_attempts` Максимальное количество попыток разморозки аккаунта. По умолчанию: 5.
|
105
119
|
- `auto_relogin` Если включено, то при невалидном токене (`BAD_TOKEN`) и предоставленных данных для авторизации (имя пользователя, пароль и totp_secret) будет произведен автоматический релогин (замена токена). Включено по умолчанию.
|
106
|
-
- `update_account_info_on_startup` Если включено, то на старте будет автоматически запрошена информация об
|
120
|
+
- `update_account_info_on_startup` Если включено, то на старте будет автоматически запрошена информация об аккаунте, а также установлен его статус. Включено по умолчанию.
|
107
121
|
- `**session_kwargs` Любые параметры, которые может принимать сессия `curl_cffi.requests.AsyncSession`. Например, можно передать параметр `proxy`.
|
108
122
|
|
109
123
|
Пример настройки клиента:
|
@@ -6,12 +6,19 @@
|
|
6
6
|
|
7
7
|
A modern, easy to use, feature-rich, and async ready API wrapper for Twitter's user API written in Python.
|
8
8
|
|
9
|
+
_NEW!_ Менеджер аккаунтов с базой данных:
|
10
|
+
- [tweepy-manager](https://github.com/alenkimov/tweepy-manager)
|
11
|
+
|
9
12
|
More libraries of the family:
|
10
13
|
- [better-proxy](https://github.com/alenkimov/better_proxy)
|
11
14
|
- [better-web3](https://github.com/alenkimov/better_web3)
|
12
15
|
|
13
16
|
Отдельное спасибо [Кузнице Ботов](https://t.me/bots_forge) за код для авторизации и разморозки! Подписывайтесь на их Telegram :)
|
14
17
|
|
18
|
+
Похожие библиотеки:
|
19
|
+
- [twikit (sync and async)](https://github.com/d60/twikit)
|
20
|
+
- [twitter-api-client (sync)](https://github.com/trevorhobenshield/twitter-api-client)
|
21
|
+
|
15
22
|
## Key Features
|
16
23
|
- Modern Pythonic API using async and await.
|
17
24
|
- Prevents user account automation detection.
|
@@ -58,7 +65,9 @@ logger.enable("twitter")
|
|
58
65
|
`level="DEBUG"` позволяет увидеть информацию обо всех запросах.
|
59
66
|
|
60
67
|
### Аккаунт
|
61
|
-
|
68
|
+
`twitter.Account`
|
69
|
+
|
70
|
+
#### Статусы аккаунта
|
62
71
|
- `UNKNOWN` - Статус аккаунта не установлен. Это статус по умолчанию.
|
63
72
|
- `BAD_TOKEN` - Неверный или мертвый токен.
|
64
73
|
- `SUSPENDED` - Действие учетной записи приостановлено. Тем не менее возможен запрос данных, а также авторизация через OAuth и OAuth2.
|
@@ -66,18 +75,23 @@ logger.enable("twitter")
|
|
66
75
|
- `CONSENT_LOCKED` - Учетная запись заморожена (лок). Условия для разморозки неизвестны.
|
67
76
|
- `GOOD` - Аккаунт в порядке.
|
68
77
|
|
78
|
+
Метод `Client.establish_status()` устанавливает статус аккаунта.
|
79
|
+
Также статус аккаунта может изменить любое взаимодействие с Twitter.
|
80
|
+
Поэтому, во время работы может внезапно быть вызвано исключение семейства `twitter.errors.BadAccount`.
|
81
|
+
|
69
82
|
Не каждое взаимодействие с Twitter достоверно определяет статус аккаунта.
|
70
|
-
Например, простой запрос данных об аккаунте честно вернет
|
83
|
+
Например, простой запрос данных об аккаунте честно вернет данные и не изменит статус, даже если действие вашей учетной записи приостановлено (`SUSPENDED`).
|
71
84
|
|
72
|
-
|
85
|
+
### Клиент
|
86
|
+
`twitter.Client`
|
73
87
|
|
74
|
-
|
75
|
-
|
88
|
+
#### Настройка
|
89
|
+
Клиент может быть сконфигурирован перед работой. Он принимает в себя следующие параметры:
|
76
90
|
- `wait_on_rate_limit` Если включено, то при достижении Rate Limit будет ждать, вместо того, чтобы выбрасывать исключение. Включено по умолчанию.
|
77
91
|
- `capsolver_api_key` API ключ сервиса [CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=m-aE3NeBGZLU). Нужен для автоматической разморозки аккаунта.
|
78
92
|
- `max_unlock_attempts` Максимальное количество попыток разморозки аккаунта. По умолчанию: 5.
|
79
93
|
- `auto_relogin` Если включено, то при невалидном токене (`BAD_TOKEN`) и предоставленных данных для авторизации (имя пользователя, пароль и totp_secret) будет произведен автоматический релогин (замена токена). Включено по умолчанию.
|
80
|
-
- `update_account_info_on_startup` Если включено, то на старте будет автоматически запрошена информация об
|
94
|
+
- `update_account_info_on_startup` Если включено, то на старте будет автоматически запрошена информация об аккаунте, а также установлен его статус. Включено по умолчанию.
|
81
95
|
- `**session_kwargs` Любые параметры, которые может принимать сессия `curl_cffi.requests.AsyncSession`. Например, можно передать параметр `proxy`.
|
82
96
|
|
83
97
|
Пример настройки клиента:
|
@@ -22,12 +22,12 @@ from .errors import (
|
|
22
22
|
RateLimited,
|
23
23
|
ServerError,
|
24
24
|
BadAccount,
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
BadAccountToken,
|
26
|
+
AccountLocked,
|
27
|
+
AccountConsentLocked,
|
28
|
+
AccountSuspended,
|
29
|
+
AccountNotFound,
|
29
30
|
)
|
30
|
-
from .utils import to_json
|
31
31
|
from .base import BaseHTTPClient
|
32
32
|
from .account import Account, AccountStatus
|
33
33
|
from .models import User, Tweet, Media, Subtask
|
@@ -95,6 +95,8 @@ class Client(BaseHTTPClient):
|
|
95
95
|
self.auto_relogin = auto_relogin
|
96
96
|
self._update_account_info_on_startup = update_account_info_on_startup
|
97
97
|
|
98
|
+
self.gql = GQLClient(self)
|
99
|
+
|
98
100
|
async def __aenter__(self):
|
99
101
|
await self.on_startup()
|
100
102
|
return await super().__aenter__()
|
@@ -114,16 +116,21 @@ class Client(BaseHTTPClient):
|
|
114
116
|
|
115
117
|
if bearer:
|
116
118
|
headers["authorization"] = f"Bearer {self._BEARER_TOKEN}"
|
117
|
-
# headers["x-twitter-auth-type"] = "OAuth2Session"
|
118
119
|
|
119
120
|
if auth:
|
120
121
|
if not self.account.auth_token:
|
121
122
|
raise ValueError("No auth_token. Login before")
|
122
123
|
|
123
124
|
cookies["auth_token"] = self.account.auth_token
|
125
|
+
headers["x-twitter-auth-type"] = "OAuth2Session"
|
124
126
|
if self.account.ct0:
|
125
127
|
cookies["ct0"] = self.account.ct0
|
126
128
|
headers["x-csrf-token"] = self.account.ct0
|
129
|
+
else:
|
130
|
+
if "auth_token" in cookies:
|
131
|
+
del cookies["auth_token"]
|
132
|
+
if "x-twitter-auth-type" in headers:
|
133
|
+
del headers["x-twitter-auth-type"]
|
127
134
|
|
128
135
|
# fmt: off
|
129
136
|
log_message = (f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
|
@@ -172,9 +179,9 @@ class Client(BaseHTTPClient):
|
|
172
179
|
if isinstance(data, dict) and "errors" in data:
|
173
180
|
exc = HTTPException(response, data)
|
174
181
|
|
175
|
-
if 141 in exc.api_codes:
|
182
|
+
if 141 in exc.api_codes or 37 in exc.api_codes:
|
176
183
|
self.account.status = AccountStatus.SUSPENDED
|
177
|
-
raise
|
184
|
+
raise AccountSuspended(exc, self.account)
|
178
185
|
|
179
186
|
if 326 in exc.api_codes:
|
180
187
|
for error_data in exc.api_errors:
|
@@ -184,23 +191,29 @@ class Client(BaseHTTPClient):
|
|
184
191
|
== "/i/flow/consent_flow"
|
185
192
|
):
|
186
193
|
self.account.status = AccountStatus.CONSENT_LOCKED
|
187
|
-
raise
|
194
|
+
raise AccountConsentLocked(exc, self.account)
|
188
195
|
|
189
196
|
self.account.status = AccountStatus.LOCKED
|
190
|
-
raise
|
197
|
+
raise AccountLocked(exc, self.account)
|
191
198
|
raise exc
|
192
199
|
|
193
200
|
return response, data
|
194
201
|
|
195
202
|
if response.status_code == 400:
|
196
|
-
|
203
|
+
exc = BadRequest(response, data)
|
204
|
+
|
205
|
+
if 399 in exc.api_codes:
|
206
|
+
self.account.status = AccountStatus.NOT_FOUND
|
207
|
+
raise AccountNotFound(exc, self.account)
|
208
|
+
|
209
|
+
raise exc
|
197
210
|
|
198
211
|
if response.status_code == 401:
|
199
212
|
exc = Unauthorized(response, data)
|
200
213
|
|
201
214
|
if 32 in exc.api_codes:
|
202
215
|
self.account.status = AccountStatus.BAD_TOKEN
|
203
|
-
raise
|
216
|
+
raise BadAccountToken(exc, self.account)
|
204
217
|
|
205
218
|
raise exc
|
206
219
|
|
@@ -209,7 +222,7 @@ class Client(BaseHTTPClient):
|
|
209
222
|
|
210
223
|
if 64 in exc.api_codes:
|
211
224
|
self.account.status = AccountStatus.SUSPENDED
|
212
|
-
raise
|
225
|
+
raise AccountSuspended(exc, self.account)
|
213
226
|
|
214
227
|
if 326 in exc.api_codes:
|
215
228
|
for error_data in exc.api_errors:
|
@@ -218,10 +231,10 @@ class Client(BaseHTTPClient):
|
|
218
231
|
and error_data.get("bounce_location") == "/i/flow/consent_flow"
|
219
232
|
):
|
220
233
|
self.account.status = AccountStatus.CONSENT_LOCKED
|
221
|
-
raise
|
234
|
+
raise AccountConsentLocked(exc, self.account)
|
222
235
|
|
223
236
|
self.account.status = AccountStatus.LOCKED
|
224
|
-
raise
|
237
|
+
raise AccountLocked(exc, self.account)
|
225
238
|
|
226
239
|
raise exc
|
227
240
|
|
@@ -261,19 +274,20 @@ class Client(BaseHTTPClient):
|
|
261
274
|
*,
|
262
275
|
auto_unlock: bool = True,
|
263
276
|
auto_relogin: bool = None,
|
277
|
+
rerequest_on_bad_ct0: bool = True,
|
264
278
|
**kwargs,
|
265
279
|
) -> tuple[requests.Response, Any]:
|
266
280
|
try:
|
267
281
|
return await self._request(method, url, **kwargs)
|
268
282
|
|
269
|
-
except
|
283
|
+
except AccountLocked:
|
270
284
|
if not self.capsolver_api_key or not auto_unlock:
|
271
285
|
raise
|
272
286
|
|
273
287
|
await self.unlock()
|
274
288
|
return await self._request(method, url, **kwargs)
|
275
289
|
|
276
|
-
except
|
290
|
+
except BadAccountToken:
|
277
291
|
if auto_relogin is None:
|
278
292
|
auto_relogin = self.auto_relogin
|
279
293
|
if (
|
@@ -284,11 +298,17 @@ class Client(BaseHTTPClient):
|
|
284
298
|
raise
|
285
299
|
|
286
300
|
await self.relogin()
|
287
|
-
return await self.
|
301
|
+
return await self.request(method, url, auto_relogin=False, **kwargs)
|
288
302
|
|
289
303
|
except Forbidden as exc:
|
290
|
-
if
|
291
|
-
|
304
|
+
if (
|
305
|
+
rerequest_on_bad_ct0
|
306
|
+
and 353 in exc.api_codes
|
307
|
+
and "ct0" in exc.response.cookies
|
308
|
+
):
|
309
|
+
return await self.request(
|
310
|
+
method, url, rerequest_on_bad_ct0=False, **kwargs
|
311
|
+
)
|
292
312
|
else:
|
293
313
|
raise
|
294
314
|
|
@@ -447,9 +467,9 @@ class Client(BaseHTTPClient):
|
|
447
467
|
"withAuxiliaryUserLabels": False,
|
448
468
|
}
|
449
469
|
params = {
|
450
|
-
"variables":
|
451
|
-
"features":
|
452
|
-
"fieldToggles":
|
470
|
+
"variables": variables,
|
471
|
+
"features": features,
|
472
|
+
"fieldToggles": field_toggles,
|
453
473
|
}
|
454
474
|
response, data = await self.request("GET", url, params=params)
|
455
475
|
if not data["data"]:
|
@@ -915,8 +935,8 @@ class Client(BaseHTTPClient):
|
|
915
935
|
"responsive_web_enhance_cards_enabled": False,
|
916
936
|
}
|
917
937
|
params = {
|
918
|
-
"variables":
|
919
|
-
"features":
|
938
|
+
"variables": variables,
|
939
|
+
"features": features,
|
920
940
|
}
|
921
941
|
response, response_json = await self.request("GET", url, params=params)
|
922
942
|
|
@@ -1008,8 +1028,8 @@ class Client(BaseHTTPClient):
|
|
1008
1028
|
"responsive_web_enhance_cards_enabled": False,
|
1009
1029
|
}
|
1010
1030
|
query = {
|
1011
|
-
"variables":
|
1012
|
-
"features":
|
1031
|
+
"variables": variables,
|
1032
|
+
"features": features,
|
1013
1033
|
}
|
1014
1034
|
response, data = await self.request("GET", url, params=query)
|
1015
1035
|
instructions = data["data"]["threaded_conversation_with_injections_v2"]["instructions"] # type: ignore
|
@@ -1053,7 +1073,7 @@ class Client(BaseHTTPClient):
|
|
1053
1073
|
"responsive_web_media_download_video_enabled": False,
|
1054
1074
|
"responsive_web_enhance_cards_enabled": False,
|
1055
1075
|
}
|
1056
|
-
params = {"variables":
|
1076
|
+
params = {"variables": variables, "features": features}
|
1057
1077
|
response, data = await self.request("GET", url, params=params)
|
1058
1078
|
|
1059
1079
|
instructions = data["data"]["user"]["result"]["timeline_v2"]["timeline"][
|
@@ -1633,12 +1653,12 @@ class Client(BaseHTTPClient):
|
|
1633
1653
|
|
1634
1654
|
:return: guest_token
|
1635
1655
|
"""
|
1636
|
-
|
1637
|
-
|
1638
|
-
|
1639
|
-
|
1640
|
-
|
1641
|
-
return guest_token
|
1656
|
+
response, data = await self._request(
|
1657
|
+
"POST",
|
1658
|
+
"https://api.twitter.com/1.1/guest/activate.json",
|
1659
|
+
auth=False,
|
1660
|
+
)
|
1661
|
+
return data["guest_token"]
|
1642
1662
|
|
1643
1663
|
async def _login(self) -> bool:
|
1644
1664
|
update_backup_code = False
|
@@ -1658,28 +1678,37 @@ class Client(BaseHTTPClient):
|
|
1658
1678
|
flow_token, subtasks = await self._login_enter_password(flow_token)
|
1659
1679
|
flow_token, subtasks = await self._account_duplication_check(flow_token)
|
1660
1680
|
|
1661
|
-
|
1662
|
-
|
1663
|
-
|
1664
|
-
|
1665
|
-
|
1666
|
-
)
|
1667
|
-
|
1668
|
-
try:
|
1669
|
-
# fmt: off
|
1670
|
-
flow_token, subtasks = await self._login_acid(flow_token, self.account.email)
|
1671
|
-
# fmt: on
|
1672
|
-
except HTTPException as exc:
|
1673
|
-
if 399 in exc.api_codes:
|
1674
|
-
logger.warning(
|
1675
|
-
f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
|
1676
|
-
f" Bad email!"
|
1681
|
+
for subtask in subtasks:
|
1682
|
+
if subtask.id == "LoginAcid":
|
1683
|
+
if not self.account.email:
|
1684
|
+
raise TwitterException(
|
1685
|
+
f"Failed to login. Task id: LoginAcid." f" No email!"
|
1677
1686
|
)
|
1687
|
+
|
1688
|
+
if subtask.primary_text == "Check your email":
|
1678
1689
|
raise TwitterException(
|
1679
|
-
f"Failed to login. Task id: LoginAcid.
|
1690
|
+
f"Failed to login. Task id: LoginAcid."
|
1691
|
+
f" Email verification required!"
|
1692
|
+
f" No IMAP handler for this version of library :<"
|
1680
1693
|
)
|
1681
|
-
|
1682
|
-
|
1694
|
+
|
1695
|
+
try:
|
1696
|
+
# fmt: off
|
1697
|
+
flow_token, subtasks = await self._login_acid(flow_token, self.account.email)
|
1698
|
+
# fmt: on
|
1699
|
+
except HTTPException as exc:
|
1700
|
+
if 399 in exc.api_codes:
|
1701
|
+
logger.warning(
|
1702
|
+
f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
|
1703
|
+
f" Bad email!"
|
1704
|
+
)
|
1705
|
+
raise TwitterException(
|
1706
|
+
f"Failed to login. Task id: LoginAcid. Bad email!"
|
1707
|
+
)
|
1708
|
+
else:
|
1709
|
+
raise
|
1710
|
+
|
1711
|
+
subtask_ids = {subtask.id for subtask in subtasks}
|
1683
1712
|
|
1684
1713
|
if "LoginTwoFactorAuthChallenge" in subtask_ids:
|
1685
1714
|
if not self.account.totp_secret:
|
@@ -1917,3 +1946,148 @@ class Client(BaseHTTPClient):
|
|
1917
1946
|
raise ValueError("Password required to enable TOTP")
|
1918
1947
|
|
1919
1948
|
await self._enable_totp()
|
1949
|
+
|
1950
|
+
|
1951
|
+
class GQLClient:
|
1952
|
+
_GRAPHQL_URL = "https://twitter.com/i/api/graphql"
|
1953
|
+
_OPERATION_TO_QUERY_ID = {
|
1954
|
+
"CreateRetweet": "ojPdsZsimiJrUGLR1sjUtA",
|
1955
|
+
"FavoriteTweet": "lI07N6Otwv1PhnEgXILM7A",
|
1956
|
+
"UnfavoriteTweet": "ZYKSe-w7KEslx3JhSIk5LA",
|
1957
|
+
"CreateTweet": "v0en1yVV-Ybeek8ClmXwYw",
|
1958
|
+
"TweetResultByRestId": "V3vfsYzNEyD9tsf4xoFRgw",
|
1959
|
+
"ModerateTweet": "p'jF:GVqCjTcZol0xcBJjw",
|
1960
|
+
"DeleteTweet": "VaenaVgh5q5ih7kvyVjgtg",
|
1961
|
+
"UserTweets": "V1ze5q3ijDS1VeLwLY0m7g",
|
1962
|
+
"TweetDetail": "VWFGPVAGkZMGRKGe3GFFnA",
|
1963
|
+
"ProfileSpotlightsQuery": "9zwVLJ48lmVUk8u_Gh9DmA",
|
1964
|
+
"Following": "t-BPOrMIduGUJWO_LxcvNQ",
|
1965
|
+
"Followers": "3yX7xr2hKjcZYnXt6cU6lQ",
|
1966
|
+
"UserByScreenName": "G3KGOASz96M-Qu0nwmGXNg",
|
1967
|
+
"UsersByRestIds": "itEhGywpgX9b3GJCzOtSrA",
|
1968
|
+
"Viewer": "W62NnYgkgziw9bwyoVht0g",
|
1969
|
+
}
|
1970
|
+
_DEFAULT_VARIABLES = {
|
1971
|
+
"count": 1000,
|
1972
|
+
"withSafetyModeUserFields": True,
|
1973
|
+
"includePromotedContent": True,
|
1974
|
+
"withQuickPromoteEligibilityTweetFields": True,
|
1975
|
+
"withVoice": True,
|
1976
|
+
"withV2Timeline": True,
|
1977
|
+
"withDownvotePerspective": False,
|
1978
|
+
"withBirdwatchNotes": True,
|
1979
|
+
"withCommunity": True,
|
1980
|
+
"withSuperFollowsUserFields": True,
|
1981
|
+
"withReactionsMetadata": False,
|
1982
|
+
"withReactionsPerspective": False,
|
1983
|
+
"withSuperFollowsTweetFields": True,
|
1984
|
+
"isMetatagsQuery": False,
|
1985
|
+
"withReplays": True,
|
1986
|
+
"withClientEventToken": False,
|
1987
|
+
"withAttachments": True,
|
1988
|
+
"withConversationQueryHighlights": True,
|
1989
|
+
"withMessageQueryHighlights": True,
|
1990
|
+
"withMessages": True,
|
1991
|
+
}
|
1992
|
+
_DEFAULT_FEATURES = {
|
1993
|
+
"c9s_tweet_anatomy_moderator_badge_enabled": True,
|
1994
|
+
"responsive_web_home_pinned_timelines_enabled": True,
|
1995
|
+
"blue_business_profile_image_shape_enabled": True,
|
1996
|
+
"creator_subscriptions_tweet_preview_api_enabled": True,
|
1997
|
+
"freedom_of_speech_not_reach_fetch_enabled": True,
|
1998
|
+
"graphql_is_translatable_rweb_tweet_is_translatable_enabled": True,
|
1999
|
+
"graphql_timeline_v2_bookmark_timeline": True,
|
2000
|
+
"hidden_profile_likes_enabled": True,
|
2001
|
+
"highlights_tweets_tab_ui_enabled": True,
|
2002
|
+
"interactive_text_enabled": True,
|
2003
|
+
"longform_notetweets_consumption_enabled": True,
|
2004
|
+
"longform_notetweets_inline_media_enabled": True,
|
2005
|
+
"longform_notetweets_rich_text_read_enabled": True,
|
2006
|
+
"longform_notetweets_richtext_consumption_enabled": True,
|
2007
|
+
"profile_foundations_tweet_stats_enabled": True,
|
2008
|
+
"profile_foundations_tweet_stats_tweet_frequency": True,
|
2009
|
+
"responsive_web_birdwatch_note_limit_enabled": True,
|
2010
|
+
"responsive_web_edit_tweet_api_enabled": True,
|
2011
|
+
"responsive_web_enhance_cards_enabled": False,
|
2012
|
+
"responsive_web_graphql_exclude_directive_enabled": True,
|
2013
|
+
"responsive_web_graphql_skip_user_profile_image_extensions_enabled": False,
|
2014
|
+
"responsive_web_graphql_timeline_navigation_enabled": True,
|
2015
|
+
"responsive_web_media_download_video_enabled": False,
|
2016
|
+
"responsive_web_text_conversations_enabled": False,
|
2017
|
+
"responsive_web_twitter_article_data_v2_enabled": True,
|
2018
|
+
"responsive_web_twitter_article_tweet_consumption_enabled": False,
|
2019
|
+
"responsive_web_twitter_blue_verified_badge_is_enabled": True,
|
2020
|
+
"rweb_lists_timeline_redesign_enabled": True,
|
2021
|
+
"spaces_2022_h2_clipping": True,
|
2022
|
+
"spaces_2022_h2_spaces_communities": True,
|
2023
|
+
"standardized_nudges_misinfo": True,
|
2024
|
+
"subscriptions_verification_info_verified_since_enabled": True,
|
2025
|
+
"tweet_awards_web_tipping_enabled": False,
|
2026
|
+
"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": True,
|
2027
|
+
"tweetypie_unmention_optimization_enabled": True,
|
2028
|
+
"verified_phone_label_enabled": False,
|
2029
|
+
"vibe_api_enabled": True,
|
2030
|
+
"view_counts_everywhere_api_enabled": True,
|
2031
|
+
"hidden_profile_subscriptions_enabled": True,
|
2032
|
+
"subscriptions_verification_info_is_identity_verified_enabled": True,
|
2033
|
+
}
|
2034
|
+
|
2035
|
+
@classmethod
|
2036
|
+
def _operation_to_url(cls, operation: str) -> tuple[str, str]:
|
2037
|
+
"""
|
2038
|
+
:return: URL and Query ID
|
2039
|
+
"""
|
2040
|
+
query_id = cls._OPERATION_TO_QUERY_ID[operation]
|
2041
|
+
url = f"{cls._GRAPHQL_URL}/{query_id}/{operation}"
|
2042
|
+
return url, query_id
|
2043
|
+
|
2044
|
+
def __init__(self, client: Client):
|
2045
|
+
self._client = client
|
2046
|
+
|
2047
|
+
async def gql_request(
|
2048
|
+
self, method, operation, **kwargs
|
2049
|
+
) -> tuple[requests.Response, dict]:
|
2050
|
+
url, query_id = self._operation_to_url(operation)
|
2051
|
+
|
2052
|
+
if method == "POST":
|
2053
|
+
payload = kwargs["json"] = kwargs.get("json") or {}
|
2054
|
+
payload["queryId"] = query_id
|
2055
|
+
else:
|
2056
|
+
params = kwargs["params"] = kwargs.get("params") or {}
|
2057
|
+
...
|
2058
|
+
|
2059
|
+
response, data = await self._client.request(method, url, **kwargs)
|
2060
|
+
return response, data["data"]
|
2061
|
+
|
2062
|
+
async def user_by_username(self, username: str) -> User | None:
|
2063
|
+
features = self._DEFAULT_FEATURES
|
2064
|
+
variables = self._DEFAULT_VARIABLES
|
2065
|
+
variables["screen_name"] = username
|
2066
|
+
params = {
|
2067
|
+
"variables": variables,
|
2068
|
+
"features": features,
|
2069
|
+
}
|
2070
|
+
response, data = await self.gql_request(
|
2071
|
+
"GET", "UserByScreenName", params=params
|
2072
|
+
)
|
2073
|
+
return User.from_raw_data(data["user"]["result"]) if data else None
|
2074
|
+
|
2075
|
+
async def users_by_ids(
|
2076
|
+
self, user_ids: Iterable[str | int]
|
2077
|
+
) -> dict[int : User | Account]:
|
2078
|
+
features = self._DEFAULT_FEATURES
|
2079
|
+
variables = self._DEFAULT_VARIABLES
|
2080
|
+
variables["userIds"] = list({str(user_id) for user_id in user_ids})
|
2081
|
+
params = {
|
2082
|
+
"variables": variables,
|
2083
|
+
"features": features,
|
2084
|
+
}
|
2085
|
+
response, data = await self.gql_request("GET", "UsersByRestIds", params=params)
|
2086
|
+
|
2087
|
+
users = {}
|
2088
|
+
for user_data in data["users"]:
|
2089
|
+
user = User.from_raw_data(user_data["result"])
|
2090
|
+
users[user.id] = user
|
2091
|
+
if user.id == self._client.account.id:
|
2092
|
+
users[self._client.account.id] = self._client.account
|
2093
|
+
return users
|
@@ -13,10 +13,11 @@ __all__ = [
|
|
13
13
|
"RateLimited",
|
14
14
|
"ServerError",
|
15
15
|
"BadAccount",
|
16
|
-
"
|
17
|
-
"
|
18
|
-
"
|
19
|
-
"
|
16
|
+
"BadAccountToken",
|
17
|
+
"AccountLocked",
|
18
|
+
"AccountConsentLocked",
|
19
|
+
"AccountSuspended",
|
20
|
+
"AccountNotFound",
|
20
21
|
]
|
21
22
|
|
22
23
|
|
@@ -149,7 +150,7 @@ class BadAccount(TwitterException):
|
|
149
150
|
super().__init__(exception_message)
|
150
151
|
|
151
152
|
|
152
|
-
class
|
153
|
+
class BadAccountToken(BadAccount):
|
153
154
|
def __init__(self, http_exception: "HTTPException", account: Account):
|
154
155
|
exception_message = (
|
155
156
|
"Bad Twitter account's auth_token. Relogin to get new token."
|
@@ -157,7 +158,7 @@ class BadToken(BadAccount):
|
|
157
158
|
super().__init__(http_exception, account, exception_message)
|
158
159
|
|
159
160
|
|
160
|
-
class
|
161
|
+
class AccountLocked(BadAccount):
|
161
162
|
def __init__(self, http_exception: "HTTPException", account: Account):
|
162
163
|
exception_message = (
|
163
164
|
f"Twitter account is locked."
|
@@ -166,13 +167,19 @@ class Locked(BadAccount):
|
|
166
167
|
super().__init__(http_exception, account, exception_message)
|
167
168
|
|
168
169
|
|
169
|
-
class
|
170
|
+
class AccountConsentLocked(BadAccount):
|
170
171
|
def __init__(self, http_exception: "HTTPException", account: Account):
|
171
172
|
exception_message = f"Twitter account is locked."
|
172
173
|
super().__init__(http_exception, account, exception_message)
|
173
174
|
|
174
175
|
|
175
|
-
class
|
176
|
+
class AccountSuspended(BadAccount):
|
176
177
|
def __init__(self, http_exception: "HTTPException", account: Account):
|
177
178
|
exception_message = f"Twitter account is suspended."
|
178
179
|
super().__init__(http_exception, account, exception_message)
|
180
|
+
|
181
|
+
|
182
|
+
class AccountNotFound(BadAccount):
|
183
|
+
def __init__(self, http_exception: "HTTPException", account: Account):
|
184
|
+
exception_message = f"Twitter account not found or deleted."
|
185
|
+
super().__init__(http_exception, account, exception_message)
|
@@ -26,6 +26,9 @@ class Media(BaseModel):
|
|
26
26
|
def __str__(self):
|
27
27
|
return str(self.id)
|
28
28
|
|
29
|
+
def __hash__(self):
|
30
|
+
return hash(self.id)
|
31
|
+
|
29
32
|
|
30
33
|
class User(BaseModel):
|
31
34
|
# fmt: off
|
@@ -46,6 +49,9 @@ class User(BaseModel):
|
|
46
49
|
def __repr__(self):
|
47
50
|
return f"{self.__class__.__name__}(id={self.id}, username={self.username})"
|
48
51
|
|
52
|
+
def __hash__(self):
|
53
|
+
return hash(self.id)
|
54
|
+
|
49
55
|
@classmethod
|
50
56
|
def from_raw_data(cls, data: dict):
|
51
57
|
legacy = data["legacy"]
|
@@ -105,6 +111,9 @@ class Tweet(BaseModel):
|
|
105
111
|
def __repr__(self):
|
106
112
|
return f"{self.__class__.__name__}(id={self.id}, user_id={self.user.id})"
|
107
113
|
|
114
|
+
def __hash__(self):
|
115
|
+
return hash(self.id)
|
116
|
+
|
108
117
|
@property
|
109
118
|
def short_text(self) -> str:
|
110
119
|
return f"{self.text[:32]}..." if len(self.text) > 16 else self.text
|
@@ -162,6 +171,9 @@ class Subtask(BaseModel):
|
|
162
171
|
detail_text: Optional[str] = None
|
163
172
|
raw_data: dict
|
164
173
|
|
174
|
+
def __hash__(self):
|
175
|
+
return hash(self.id)
|
176
|
+
|
165
177
|
@classmethod
|
166
178
|
def from_raw_data(cls, data: dict) -> "Subtask":
|
167
179
|
task = {"id": data["subtask_id"]}
|
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
|
File without changes
|