tweepy-self 1.10.0b8__py3-none-any.whl → 1.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tweepy_self-1.10.0b8.dist-info → tweepy_self-1.11.0.dist-info}/METADATA +21 -7
- {tweepy_self-1.10.0b8.dist-info → tweepy_self-1.11.0.dist-info}/RECORD +8 -8
- twitter/__init__.py +1 -1
- twitter/client.py +227 -53
- twitter/enums.py +1 -0
- twitter/errors.py +15 -8
- twitter/models.py +12 -0
- {tweepy_self-1.10.0b8.dist-info → tweepy_self-1.11.0.dist-info}/WHEEL +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
|
Пример настройки клиента:
|
@@ -1,4 +1,4 @@
|
|
1
|
-
twitter/__init__.py,sha256
|
1
|
+
twitter/__init__.py,sha256=yqTUBTKJca19jJBtmv9CML9Lfgvw1yjNew7i0f5v80Y,745
|
2
2
|
twitter/_capsolver/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
3
|
twitter/_capsolver/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
4
|
twitter/_capsolver/core/base.py,sha256=In3qDLgRh1z1UZLaLFgYcDEdnqW3d62PVzgEjU2S4BU,8883
|
@@ -10,14 +10,14 @@ twitter/account.py,sha256=joAB5Zw-Le5E3kOZ-1nb4DPGlTqWYv2Vs6gJ3cwu7is,3175
|
|
10
10
|
twitter/base/__init__.py,sha256=Q2ko0HeOS5tiBnDVKxxaZYetwRR3YXJ67ujL3oThGd4,141
|
11
11
|
twitter/base/client.py,sha256=J_iL4ZGfwTbZ2gpjtFCbBxNgt7weJ55EeMGzYsLtjf4,500
|
12
12
|
twitter/base/session.py,sha256=JFPS-9Qae1iY3NfNcywxvWWmRDijaU_Rjs3WaQ00iFA,2071
|
13
|
-
twitter/client.py,sha256=
|
14
|
-
twitter/enums.py,sha256
|
15
|
-
twitter/errors.py,sha256=
|
16
|
-
twitter/models.py,sha256=
|
13
|
+
twitter/client.py,sha256=WQFcwt9RA4LSgF0iRFLOy5-bJepyb2vLWLMX1iS2k9s,82317
|
14
|
+
twitter/enums.py,sha256=RzQnvxEmMwGVTHZ7e3P3kAE6U9btvNZ3cqVMi7y9ItI,299
|
15
|
+
twitter/errors.py,sha256=xYuwU8F-_XDGWOLvLUAT_U3sO1ZJOPQ7uOhe-SBo6vY,5610
|
16
|
+
twitter/models.py,sha256=HuiZItosr1FibjkHNWZeIsEe9K52o7OBiBuvoNu0Rxc,5905
|
17
17
|
twitter/utils/__init__.py,sha256=usxpfcRQ7zxTTgZ-i425tT7hIz73Pwh9FDj4t6O3dYg,663
|
18
18
|
twitter/utils/file.py,sha256=Sz2KEF9DnL04aOP1XabuMYMMF4VR8dJ_KWMEVvQ666Y,1120
|
19
19
|
twitter/utils/html.py,sha256=nrOJw0vUKfBaHgFaQSQIdXfvfZ8mdu84MU_s46kJTJ4,2087
|
20
20
|
twitter/utils/other.py,sha256=9RIYF2AMdmNKIwClG3jBP7zlvxZPEgYfuHaIiOhURzM,1061
|
21
|
-
tweepy_self-1.
|
22
|
-
tweepy_self-1.
|
23
|
-
tweepy_self-1.
|
21
|
+
tweepy_self-1.11.0.dist-info/METADATA,sha256=WKDAyHV76Ewun0b94z7dRHet4iLp8O5YWXWP31euv28,13754
|
22
|
+
tweepy_self-1.11.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
23
|
+
tweepy_self-1.11.0.dist-info/RECORD,,
|
twitter/__init__.py
CHANGED
twitter/client.py
CHANGED
@@ -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
|
twitter/enums.py
CHANGED
twitter/errors.py
CHANGED
@@ -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)
|
twitter/models.py
CHANGED
@@ -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
|