tweepy-self 1.10.0b9__py3-none-any.whl → 1.11.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.10.0b9.dist-info → tweepy_self-1.11.1.dist-info}/METADATA +21 -7
- {tweepy_self-1.10.0b9.dist-info → tweepy_self-1.11.1.dist-info}/RECORD +10 -10
- twitter/__init__.py +1 -1
- twitter/account.py +1 -1
- twitter/base/session.py +4 -3
- twitter/client.py +235 -67
- twitter/enums.py +1 -0
- twitter/errors.py +15 -8
- twitter/models.py +12 -0
- {tweepy_self-1.10.0b9.dist-info → tweepy_self-1.11.1.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.1
|
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
|
@@ -6,18 +6,18 @@ twitter/_capsolver/core/config.py,sha256=8_eXT6N2hBheN2uCMNhqk8tLZRJjLDTYLK208fq
|
|
6
6
|
twitter/_capsolver/core/enum.py,sha256=ivfAEN6jrg3iaq5C3H7CuRqsvOloX1b8lF8cLa3zaiY,1741
|
7
7
|
twitter/_capsolver/core/serializer.py,sha256=xPEUIPgytuw2wM1ubTY3RMhJGVyp_d3bokPTx0BjF0c,2602
|
8
8
|
twitter/_capsolver/fun_captcha.py,sha256=VVbTmn08cGnvPMGdJmPxaLfAIPxyA68oTSAyEL8RWnU,10974
|
9
|
-
twitter/account.py,sha256=
|
9
|
+
twitter/account.py,sha256=4ILOQR9Q9V5UbpIYYjS7_BPjpBszbQx6WcfdVWnpZdM,3180
|
10
10
|
twitter/base/__init__.py,sha256=Q2ko0HeOS5tiBnDVKxxaZYetwRR3YXJ67ujL3oThGd4,141
|
11
11
|
twitter/base/client.py,sha256=J_iL4ZGfwTbZ2gpjtFCbBxNgt7weJ55EeMGzYsLtjf4,500
|
12
|
-
twitter/base/session.py,sha256=
|
13
|
-
twitter/client.py,sha256=
|
14
|
-
twitter/enums.py,sha256
|
15
|
-
twitter/errors.py,sha256=
|
16
|
-
twitter/models.py,sha256=
|
12
|
+
twitter/base/session.py,sha256=o4Rre_xpKUpHAqyIOJ57M3gn4_Bf2SksZi7uTF6s-8s,2102
|
13
|
+
twitter/client.py,sha256=4ieUEhepYxJfSMu0k8fFtmMWU4oW9HNCL7tplyu5NRk,82365
|
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.1.dist-info/METADATA,sha256=Eh-oQgZ1LcEmjq6MlT02n8xF-7ZdRK05Ndl5wHMrHPA,13754
|
22
|
+
tweepy_self-1.11.1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
23
|
+
tweepy_self-1.11.1.dist-info/RECORD,,
|
twitter/__init__.py
CHANGED
twitter/account.py
CHANGED
twitter/base/session.py
CHANGED
@@ -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
|
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
|
@@ -41,17 +41,17 @@ from .utils import (
|
|
41
41
|
class Client(BaseHTTPClient):
|
42
42
|
_BEARER_TOKEN = "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"
|
43
43
|
_DEFAULT_HEADERS = {
|
44
|
-
"authority": "
|
45
|
-
"origin": "https://
|
44
|
+
"authority": "x.com",
|
45
|
+
"origin": "https://x.com",
|
46
46
|
"x-twitter-active-user": "yes",
|
47
47
|
"x-twitter-client-language": "en",
|
48
48
|
}
|
49
|
-
_GRAPHQL_URL = "https://
|
49
|
+
_GRAPHQL_URL = "https://x.com/i/api/graphql"
|
50
50
|
_ACTION_TO_QUERY_ID = {
|
51
51
|
"CreateRetweet": "ojPdsZsimiJrUGLR1sjUtA",
|
52
52
|
"FavoriteTweet": "lI07N6Otwv1PhnEgXILM7A",
|
53
53
|
"UnfavoriteTweet": "ZYKSe-w7KEslx3JhSIk5LA",
|
54
|
-
"CreateTweet": "
|
54
|
+
"CreateTweet": "oB-5XsHNAbjvARJEc8CZFw",
|
55
55
|
"TweetResultByRestId": "V3vfsYzNEyD9tsf4xoFRgw",
|
56
56
|
"ModerateTweet": "p'jF:GVqCjTcZol0xcBJjw",
|
57
57
|
"DeleteTweet": "VaenaVgh5q5ih7kvyVjgtg",
|
@@ -62,9 +62,9 @@ class Client(BaseHTTPClient):
|
|
62
62
|
"Followers": "3yX7xr2hKjcZYnXt6cU6lQ",
|
63
63
|
"UserByScreenName": "G3KGOASz96M-Qu0nwmGXNg",
|
64
64
|
"UsersByRestIds": "itEhGywpgX9b3GJCzOtSrA",
|
65
|
-
"Viewer": "
|
65
|
+
"Viewer": "-876iyxD1O_0X0BqeykjZA",
|
66
66
|
}
|
67
|
-
_CAPTCHA_URL = "https://
|
67
|
+
_CAPTCHA_URL = "https://x.com/account/access"
|
68
68
|
_CAPTCHA_SITE_KEY = "0152B4EB-D2DC-460A-89A1-629838B529C9"
|
69
69
|
|
70
70
|
@classmethod
|
@@ -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__()
|
@@ -109,21 +111,25 @@ class Client(BaseHTTPClient):
|
|
109
111
|
wait_on_rate_limit: bool = None,
|
110
112
|
**kwargs,
|
111
113
|
) -> tuple[requests.Response, Any]:
|
112
|
-
cookies = kwargs["cookies"] = kwargs.get("cookies"
|
113
|
-
headers = kwargs["headers"] = kwargs.get("headers"
|
114
|
-
|
114
|
+
cookies = kwargs["cookies"] = kwargs.get("cookies", {})
|
115
|
+
headers = kwargs["headers"] = kwargs.get("headers", {})
|
115
116
|
if bearer:
|
116
117
|
headers["authorization"] = f"Bearer {self._BEARER_TOKEN}"
|
117
|
-
# headers["x-twitter-auth-type"] = "OAuth2Session"
|
118
118
|
|
119
119
|
if auth:
|
120
120
|
if not self.account.auth_token:
|
121
121
|
raise ValueError("No auth_token. Login before")
|
122
122
|
|
123
123
|
cookies["auth_token"] = self.account.auth_token
|
124
|
+
headers["x-twitter-auth-type"] = "OAuth2Session"
|
124
125
|
if self.account.ct0:
|
125
126
|
cookies["ct0"] = self.account.ct0
|
126
127
|
headers["x-csrf-token"] = self.account.ct0
|
128
|
+
else:
|
129
|
+
if "auth_token" in cookies:
|
130
|
+
del cookies["auth_token"]
|
131
|
+
if "x-twitter-auth-type" in headers:
|
132
|
+
del headers["x-twitter-auth-type"]
|
127
133
|
|
128
134
|
# fmt: off
|
129
135
|
log_message = (f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
|
@@ -152,7 +158,7 @@ class Client(BaseHTTPClient):
|
|
152
158
|
f"\nResponse data: {data}")
|
153
159
|
# fmt: on
|
154
160
|
|
155
|
-
if ct0 := self._session.cookies.get("ct0", domain=".
|
161
|
+
if ct0 := self._session.cookies.get("ct0", domain=".x.com"):
|
156
162
|
self.account.ct0 = ct0
|
157
163
|
|
158
164
|
auth_token = self._session.cookies.get("auth_token")
|
@@ -172,9 +178,9 @@ class Client(BaseHTTPClient):
|
|
172
178
|
if isinstance(data, dict) and "errors" in data:
|
173
179
|
exc = HTTPException(response, data)
|
174
180
|
|
175
|
-
if 141 in exc.api_codes:
|
181
|
+
if 141 in exc.api_codes or 37 in exc.api_codes:
|
176
182
|
self.account.status = AccountStatus.SUSPENDED
|
177
|
-
raise
|
183
|
+
raise AccountSuspended(exc, self.account)
|
178
184
|
|
179
185
|
if 326 in exc.api_codes:
|
180
186
|
for error_data in exc.api_errors:
|
@@ -184,23 +190,29 @@ class Client(BaseHTTPClient):
|
|
184
190
|
== "/i/flow/consent_flow"
|
185
191
|
):
|
186
192
|
self.account.status = AccountStatus.CONSENT_LOCKED
|
187
|
-
raise
|
193
|
+
raise AccountConsentLocked(exc, self.account)
|
188
194
|
|
189
195
|
self.account.status = AccountStatus.LOCKED
|
190
|
-
raise
|
196
|
+
raise AccountLocked(exc, self.account)
|
191
197
|
raise exc
|
192
198
|
|
193
199
|
return response, data
|
194
200
|
|
195
201
|
if response.status_code == 400:
|
196
|
-
|
202
|
+
exc = BadRequest(response, data)
|
203
|
+
|
204
|
+
if 399 in exc.api_codes:
|
205
|
+
self.account.status = AccountStatus.NOT_FOUND
|
206
|
+
raise AccountNotFound(exc, self.account)
|
207
|
+
|
208
|
+
raise exc
|
197
209
|
|
198
210
|
if response.status_code == 401:
|
199
211
|
exc = Unauthorized(response, data)
|
200
212
|
|
201
213
|
if 32 in exc.api_codes:
|
202
214
|
self.account.status = AccountStatus.BAD_TOKEN
|
203
|
-
raise
|
215
|
+
raise BadAccountToken(exc, self.account)
|
204
216
|
|
205
217
|
raise exc
|
206
218
|
|
@@ -209,7 +221,7 @@ class Client(BaseHTTPClient):
|
|
209
221
|
|
210
222
|
if 64 in exc.api_codes:
|
211
223
|
self.account.status = AccountStatus.SUSPENDED
|
212
|
-
raise
|
224
|
+
raise AccountSuspended(exc, self.account)
|
213
225
|
|
214
226
|
if 326 in exc.api_codes:
|
215
227
|
for error_data in exc.api_errors:
|
@@ -218,10 +230,10 @@ class Client(BaseHTTPClient):
|
|
218
230
|
and error_data.get("bounce_location") == "/i/flow/consent_flow"
|
219
231
|
):
|
220
232
|
self.account.status = AccountStatus.CONSENT_LOCKED
|
221
|
-
raise
|
233
|
+
raise AccountConsentLocked(exc, self.account)
|
222
234
|
|
223
235
|
self.account.status = AccountStatus.LOCKED
|
224
|
-
raise
|
236
|
+
raise AccountLocked(exc, self.account)
|
225
237
|
|
226
238
|
raise exc
|
227
239
|
|
@@ -261,19 +273,20 @@ class Client(BaseHTTPClient):
|
|
261
273
|
*,
|
262
274
|
auto_unlock: bool = True,
|
263
275
|
auto_relogin: bool = None,
|
276
|
+
rerequest_on_bad_ct0: bool = True,
|
264
277
|
**kwargs,
|
265
278
|
) -> tuple[requests.Response, Any]:
|
266
279
|
try:
|
267
280
|
return await self._request(method, url, **kwargs)
|
268
281
|
|
269
|
-
except
|
282
|
+
except AccountLocked:
|
270
283
|
if not self.capsolver_api_key or not auto_unlock:
|
271
284
|
raise
|
272
285
|
|
273
286
|
await self.unlock()
|
274
287
|
return await self._request(method, url, **kwargs)
|
275
288
|
|
276
|
-
except
|
289
|
+
except BadAccountToken:
|
277
290
|
if auto_relogin is None:
|
278
291
|
auto_relogin = self.auto_relogin
|
279
292
|
if (
|
@@ -284,11 +297,17 @@ class Client(BaseHTTPClient):
|
|
284
297
|
raise
|
285
298
|
|
286
299
|
await self.relogin()
|
287
|
-
return await self.
|
300
|
+
return await self.request(method, url, auto_relogin=False, **kwargs)
|
288
301
|
|
289
302
|
except Forbidden as exc:
|
290
|
-
if
|
291
|
-
|
303
|
+
if (
|
304
|
+
rerequest_on_bad_ct0
|
305
|
+
and 353 in exc.api_codes
|
306
|
+
and "ct0" in exc.response.cookies
|
307
|
+
):
|
308
|
+
return await self.request(
|
309
|
+
method, url, rerequest_on_bad_ct0=False, **kwargs
|
310
|
+
)
|
292
311
|
else:
|
293
312
|
raise
|
294
313
|
|
@@ -307,7 +326,7 @@ class Client(BaseHTTPClient):
|
|
307
326
|
scope: str,
|
308
327
|
response_type: str,
|
309
328
|
) -> str:
|
310
|
-
url = "https://
|
329
|
+
url = "https://x.com/i/api/2/oauth2/authorize"
|
311
330
|
querystring = {
|
312
331
|
"client_id": client_id,
|
313
332
|
"code_challenge": code_challenge,
|
@@ -329,7 +348,7 @@ class Client(BaseHTTPClient):
|
|
329
348
|
headers = {"content-type": "application/x-www-form-urlencoded"}
|
330
349
|
await self.request(
|
331
350
|
"POST",
|
332
|
-
"https://
|
351
|
+
"https://x.com/i/api/2/oauth2/authorize",
|
333
352
|
headers=headers,
|
334
353
|
data=data,
|
335
354
|
)
|
@@ -373,7 +392,7 @@ class Client(BaseHTTPClient):
|
|
373
392
|
|
374
393
|
:return: Response: html страница привязки приложения (аутентификации) старого типа.
|
375
394
|
"""
|
376
|
-
url = "https://api.
|
395
|
+
url = "https://api.x.com/oauth/authenticate"
|
377
396
|
oauth_params["oauth_token"] = oauth_token
|
378
397
|
response, _ = await self.request("GET", url, params=oauth_params)
|
379
398
|
|
@@ -391,7 +410,7 @@ class Client(BaseHTTPClient):
|
|
391
410
|
authenticity_token: str,
|
392
411
|
redirect_after_login_url: str,
|
393
412
|
) -> requests.Response:
|
394
|
-
url = "https://api.
|
413
|
+
url = "https://api.x.com/oauth/authorize"
|
395
414
|
params = {
|
396
415
|
"redirect_after_login": redirect_after_login_url,
|
397
416
|
"authenticity_token": authenticity_token,
|
@@ -421,7 +440,7 @@ class Client(BaseHTTPClient):
|
|
421
440
|
return authenticity_token, redirect_url
|
422
441
|
|
423
442
|
async def _update_account_username(self):
|
424
|
-
url = "https://
|
443
|
+
url = "https://x.com/i/api/1.1/account/settings.json"
|
425
444
|
response, response_json = await self.request("POST", url)
|
426
445
|
self.account.username = response_json["screen_name"]
|
427
446
|
|
@@ -447,9 +466,9 @@ class Client(BaseHTTPClient):
|
|
447
466
|
"withAuxiliaryUserLabels": False,
|
448
467
|
}
|
449
468
|
params = {
|
450
|
-
"variables":
|
451
|
-
"features":
|
452
|
-
"fieldToggles":
|
469
|
+
"variables": variables,
|
470
|
+
"features": features,
|
471
|
+
"fieldToggles": field_toggles,
|
453
472
|
}
|
454
473
|
response, data = await self.request("GET", url, params=params)
|
455
474
|
if not data["data"]:
|
@@ -536,7 +555,7 @@ class Client(BaseHTTPClient):
|
|
536
555
|
|
537
556
|
:return: Media
|
538
557
|
"""
|
539
|
-
url = "https://upload.
|
558
|
+
url = "https://upload.x.com/1.1/media/upload.json"
|
540
559
|
payload = {"media_data": base64.b64encode(image)}
|
541
560
|
for attempt in range(attempts):
|
542
561
|
try:
|
@@ -561,7 +580,7 @@ class Client(BaseHTTPClient):
|
|
561
580
|
raise
|
562
581
|
|
563
582
|
async def _follow_action(self, action: str, user_id: int | str) -> bool:
|
564
|
-
url = f"https://
|
583
|
+
url = f"https://x.com/i/api/1.1/friendships/{action}.json"
|
565
584
|
params = {
|
566
585
|
"include_profile_interstitial_type": "1",
|
567
586
|
"include_blocking": "1",
|
@@ -691,7 +710,7 @@ class Client(BaseHTTPClient):
|
|
691
710
|
return is_deleted
|
692
711
|
|
693
712
|
async def pin_tweet(self, tweet_id: str | int) -> bool:
|
694
|
-
url = "https://api.
|
713
|
+
url = "https://api.x.com/1.1/account/pin_tweet.json"
|
695
714
|
data = {
|
696
715
|
"tweet_mode": "extended",
|
697
716
|
"id": str(tweet_id),
|
@@ -741,9 +760,12 @@ class Client(BaseHTTPClient):
|
|
741
760
|
"longform_notetweets_consumption_enabled": True,
|
742
761
|
"responsive_web_twitter_article_tweet_consumption_enabled": True,
|
743
762
|
"tweet_awards_web_tipping_enabled": False,
|
763
|
+
"creator_subscriptions_quote_tweet_preview_enabled": False,
|
744
764
|
"longform_notetweets_rich_text_read_enabled": True,
|
745
765
|
"longform_notetweets_inline_media_enabled": True,
|
766
|
+
"articles_preview_enabled": True,
|
746
767
|
"rweb_video_timestamps_enabled": True,
|
768
|
+
"rweb_tipjar_consumption_enabled": True,
|
747
769
|
"responsive_web_graphql_exclude_directive_enabled": True,
|
748
770
|
"verified_phone_label_enabled": False,
|
749
771
|
"freedom_of_speech_not_reach_fetch_enabled": True,
|
@@ -866,7 +888,7 @@ class Client(BaseHTTPClient):
|
|
866
888
|
"""
|
867
889
|
:return: Raw vote information
|
868
890
|
"""
|
869
|
-
url = "https://caps.
|
891
|
+
url = "https://caps.x.com/v2/capi/passthrough/1"
|
870
892
|
params = {
|
871
893
|
"twitter:string:card_uri": f"card://{card_id}",
|
872
894
|
"twitter:long:original_tweet_id": str(tweet_id),
|
@@ -915,8 +937,8 @@ class Client(BaseHTTPClient):
|
|
915
937
|
"responsive_web_enhance_cards_enabled": False,
|
916
938
|
}
|
917
939
|
params = {
|
918
|
-
"variables":
|
919
|
-
"features":
|
940
|
+
"variables": variables,
|
941
|
+
"features": features,
|
920
942
|
}
|
921
943
|
response, response_json = await self.request("GET", url, params=params)
|
922
944
|
|
@@ -1008,8 +1030,8 @@ class Client(BaseHTTPClient):
|
|
1008
1030
|
"responsive_web_enhance_cards_enabled": False,
|
1009
1031
|
}
|
1010
1032
|
query = {
|
1011
|
-
"variables":
|
1012
|
-
"features":
|
1033
|
+
"variables": variables,
|
1034
|
+
"features": features,
|
1013
1035
|
}
|
1014
1036
|
response, data = await self.request("GET", url, params=query)
|
1015
1037
|
instructions = data["data"]["threaded_conversation_with_injections_v2"]["instructions"] # type: ignore
|
@@ -1053,7 +1075,7 @@ class Client(BaseHTTPClient):
|
|
1053
1075
|
"responsive_web_media_download_video_enabled": False,
|
1054
1076
|
"responsive_web_enhance_cards_enabled": False,
|
1055
1077
|
}
|
1056
|
-
params = {"variables":
|
1078
|
+
params = {"variables": variables, "features": features}
|
1057
1079
|
response, data = await self.request("GET", url, params=params)
|
1058
1080
|
|
1059
1081
|
instructions = data["data"]["user"]["result"]["timeline_v2"]["timeline"][
|
@@ -1081,7 +1103,7 @@ class Client(BaseHTTPClient):
|
|
1081
1103
|
"""
|
1082
1104
|
:return: Image URL
|
1083
1105
|
"""
|
1084
|
-
url = f"https://api.
|
1106
|
+
url = f"https://api.x.com/1.1/account/update_profile_{type}.json"
|
1085
1107
|
params = {
|
1086
1108
|
"media_id": str(media_id),
|
1087
1109
|
"include_profile_interstitial_type": "1",
|
@@ -1116,7 +1138,7 @@ class Client(BaseHTTPClient):
|
|
1116
1138
|
return await self._update_profile_image("banner", media_id)
|
1117
1139
|
|
1118
1140
|
async def change_username(self, username: str) -> bool:
|
1119
|
-
url = "https://
|
1141
|
+
url = "https://x.com/i/api/1.1/account/settings.json"
|
1120
1142
|
payload = {"screen_name": username}
|
1121
1143
|
response, data = await self.request("POST", url, data=payload)
|
1122
1144
|
new_username = data["screen_name"]
|
@@ -1131,7 +1153,7 @@ class Client(BaseHTTPClient):
|
|
1131
1153
|
if not self.account.password:
|
1132
1154
|
raise ValueError(f"Specify the current password before changing it")
|
1133
1155
|
|
1134
|
-
url = "https://
|
1156
|
+
url = "https://x.com/i/api/i/account/change_password.json"
|
1135
1157
|
payload = {
|
1136
1158
|
"current_password": self.account.password,
|
1137
1159
|
"password": password,
|
@@ -1155,7 +1177,7 @@ class Client(BaseHTTPClient):
|
|
1155
1177
|
if name is None and description is None:
|
1156
1178
|
raise ValueError("Specify at least one param")
|
1157
1179
|
|
1158
|
-
url = "https://
|
1180
|
+
url = "https://x.com/i/api/1.1/account/update_profile.json"
|
1159
1181
|
# Создаем словарь data, включая в него только те ключи, для которых значения не равны None
|
1160
1182
|
payload = {
|
1161
1183
|
k: v
|
@@ -1180,7 +1202,7 @@ class Client(BaseHTTPClient):
|
|
1180
1202
|
return updated
|
1181
1203
|
|
1182
1204
|
async def establish_status(self):
|
1183
|
-
url = "https://
|
1205
|
+
url = "https://x.com/i/api/1.1/account/update_profile.json"
|
1184
1206
|
try:
|
1185
1207
|
await self.request("POST", url, auto_unlock=False, auto_relogin=False)
|
1186
1208
|
self.account.status = AccountStatus.GOOD
|
@@ -1195,7 +1217,7 @@ class Client(BaseHTTPClient):
|
|
1195
1217
|
visibility: Literal["self", "mutualfollow"] = "self",
|
1196
1218
|
year_visibility: Literal["self"] = "self",
|
1197
1219
|
) -> bool:
|
1198
|
-
url = "https://
|
1220
|
+
url = "https://x.com/i/api/1.1/account/update_profile.json"
|
1199
1221
|
payload = {
|
1200
1222
|
"birthdate_day": day,
|
1201
1223
|
"birthdate_month": month,
|
@@ -1220,7 +1242,7 @@ class Client(BaseHTTPClient):
|
|
1220
1242
|
"""
|
1221
1243
|
:return: Event data
|
1222
1244
|
"""
|
1223
|
-
url = "https://api.
|
1245
|
+
url = "https://api.x.com/1.1/direct_messages/events/new.json"
|
1224
1246
|
payload = {
|
1225
1247
|
"event": {
|
1226
1248
|
"type": "message_create",
|
@@ -1242,7 +1264,7 @@ class Client(BaseHTTPClient):
|
|
1242
1264
|
|
1243
1265
|
:return: Event data
|
1244
1266
|
"""
|
1245
|
-
url = f"https://api.
|
1267
|
+
url = f"https://api.x.com/2/dm_conversations/{conversation_id}/messages"
|
1246
1268
|
payload = {"text": text}
|
1247
1269
|
response, response_json = await self.request("POST", url, json=payload)
|
1248
1270
|
event_data = response_json["event"]
|
@@ -1252,7 +1274,7 @@ class Client(BaseHTTPClient):
|
|
1252
1274
|
"""
|
1253
1275
|
:return: Messages data
|
1254
1276
|
"""
|
1255
|
-
url = "https://
|
1277
|
+
url = "https://x.com/i/api/1.1/dm/inbox_initial_state.json"
|
1256
1278
|
params = {
|
1257
1279
|
"nsfw_filtering_enabled": "false",
|
1258
1280
|
"filter_low_quality": "false",
|
@@ -1394,7 +1416,7 @@ class Client(BaseHTTPClient):
|
|
1394
1416
|
verification_string=token,
|
1395
1417
|
)
|
1396
1418
|
|
1397
|
-
if response.url == "https://
|
1419
|
+
if response.url == "https://x.com/?lang=en":
|
1398
1420
|
break
|
1399
1421
|
|
1400
1422
|
(
|
@@ -1424,7 +1446,7 @@ class Client(BaseHTTPClient):
|
|
1424
1446
|
await self.establish_status()
|
1425
1447
|
|
1426
1448
|
async def update_backup_code(self):
|
1427
|
-
url = "https://api.
|
1449
|
+
url = "https://api.x.com/1.1/account/backup_code.json"
|
1428
1450
|
response, response_json = await self.request("GET", url)
|
1429
1451
|
self.account.backup_code = response_json["codes"][0]
|
1430
1452
|
|
@@ -1432,7 +1454,7 @@ class Client(BaseHTTPClient):
|
|
1432
1454
|
"""
|
1433
1455
|
:return: flow_token and subtasks
|
1434
1456
|
"""
|
1435
|
-
url = "https://api.
|
1457
|
+
url = "https://api.x.com/1.1/onboarding/task.json"
|
1436
1458
|
response, data = await self.request("POST", url, **request_kwargs)
|
1437
1459
|
subtasks = [
|
1438
1460
|
Subtask.from_raw_data(subtask_data) for subtask_data in data["subtasks"]
|
@@ -1609,6 +1631,7 @@ class Client(BaseHTTPClient):
|
|
1609
1631
|
async def _viewer(self):
|
1610
1632
|
url, query_id = self._action_to_url("Viewer")
|
1611
1633
|
features = {
|
1634
|
+
"rweb_tipjar_consumption_enabled": True,
|
1612
1635
|
"responsive_web_graphql_exclude_directive_enabled": True,
|
1613
1636
|
"verified_phone_label_enabled": False,
|
1614
1637
|
"creator_subscriptions_tweet_preview_api_enabled": True,
|
@@ -1633,12 +1656,12 @@ class Client(BaseHTTPClient):
|
|
1633
1656
|
|
1634
1657
|
:return: guest_token
|
1635
1658
|
"""
|
1636
|
-
|
1637
|
-
|
1638
|
-
|
1639
|
-
|
1640
|
-
|
1641
|
-
return guest_token
|
1659
|
+
response, data = await self._request(
|
1660
|
+
"POST",
|
1661
|
+
"https://api.x.com/1.1/guest/activate.json",
|
1662
|
+
auth=False,
|
1663
|
+
)
|
1664
|
+
return data["guest_token"]
|
1642
1665
|
|
1643
1666
|
async def _login(self) -> bool:
|
1644
1667
|
update_backup_code = False
|
@@ -1733,6 +1756,7 @@ class Client(BaseHTTPClient):
|
|
1733
1756
|
else:
|
1734
1757
|
raise
|
1735
1758
|
|
1759
|
+
await self._viewer()
|
1736
1760
|
await self._complete_subtask(flow_token, [])
|
1737
1761
|
return update_backup_code
|
1738
1762
|
|
@@ -1750,7 +1774,6 @@ class Client(BaseHTTPClient):
|
|
1750
1774
|
raise ValueError("No password")
|
1751
1775
|
|
1752
1776
|
update_backup_code = await self._login()
|
1753
|
-
await self._viewer()
|
1754
1777
|
|
1755
1778
|
if update_backup_code:
|
1756
1779
|
await self.update_backup_code()
|
@@ -1774,7 +1797,7 @@ class Client(BaseHTTPClient):
|
|
1774
1797
|
if not self.account.id:
|
1775
1798
|
await self.update_account_info()
|
1776
1799
|
|
1777
|
-
url = f"https://
|
1800
|
+
url = f"https://x.com/i/api/1.1/strato/column/User/{self.account.id}/account-security/twoFactorAuthSettings2"
|
1778
1801
|
response, data = await self.request("GET", url)
|
1779
1802
|
# fmt: off
|
1780
1803
|
return "Totp" in [method_data["twoFactorType"] for method_data in data["methods"]]
|
@@ -1926,3 +1949,148 @@ class Client(BaseHTTPClient):
|
|
1926
1949
|
raise ValueError("Password required to enable TOTP")
|
1927
1950
|
|
1928
1951
|
await self._enable_totp()
|
1952
|
+
|
1953
|
+
|
1954
|
+
class GQLClient:
|
1955
|
+
_GRAPHQL_URL = "https://x.com/i/api/graphql"
|
1956
|
+
_OPERATION_TO_QUERY_ID = {
|
1957
|
+
"CreateRetweet": "ojPdsZsimiJrUGLR1sjUtA",
|
1958
|
+
"FavoriteTweet": "lI07N6Otwv1PhnEgXILM7A",
|
1959
|
+
"UnfavoriteTweet": "ZYKSe-w7KEslx3JhSIk5LA",
|
1960
|
+
"CreateTweet": "v0en1yVV-Ybeek8ClmXwYw",
|
1961
|
+
"TweetResultByRestId": "V3vfsYzNEyD9tsf4xoFRgw",
|
1962
|
+
"ModerateTweet": "p'jF:GVqCjTcZol0xcBJjw",
|
1963
|
+
"DeleteTweet": "VaenaVgh5q5ih7kvyVjgtg",
|
1964
|
+
"UserTweets": "V1ze5q3ijDS1VeLwLY0m7g",
|
1965
|
+
"TweetDetail": "VWFGPVAGkZMGRKGe3GFFnA",
|
1966
|
+
"ProfileSpotlightsQuery": "9zwVLJ48lmVUk8u_Gh9DmA",
|
1967
|
+
"Following": "t-BPOrMIduGUJWO_LxcvNQ",
|
1968
|
+
"Followers": "3yX7xr2hKjcZYnXt6cU6lQ",
|
1969
|
+
"UserByScreenName": "G3KGOASz96M-Qu0nwmGXNg",
|
1970
|
+
"UsersByRestIds": "itEhGywpgX9b3GJCzOtSrA",
|
1971
|
+
"Viewer": "-876iyxD1O_0X0BqeykjZA",
|
1972
|
+
}
|
1973
|
+
_DEFAULT_VARIABLES = {
|
1974
|
+
"count": 1000,
|
1975
|
+
"withSafetyModeUserFields": True,
|
1976
|
+
"includePromotedContent": True,
|
1977
|
+
"withQuickPromoteEligibilityTweetFields": True,
|
1978
|
+
"withVoice": True,
|
1979
|
+
"withV2Timeline": True,
|
1980
|
+
"withDownvotePerspective": False,
|
1981
|
+
"withBirdwatchNotes": True,
|
1982
|
+
"withCommunity": True,
|
1983
|
+
"withSuperFollowsUserFields": True,
|
1984
|
+
"withReactionsMetadata": False,
|
1985
|
+
"withReactionsPerspective": False,
|
1986
|
+
"withSuperFollowsTweetFields": True,
|
1987
|
+
"isMetatagsQuery": False,
|
1988
|
+
"withReplays": True,
|
1989
|
+
"withClientEventToken": False,
|
1990
|
+
"withAttachments": True,
|
1991
|
+
"withConversationQueryHighlights": True,
|
1992
|
+
"withMessageQueryHighlights": True,
|
1993
|
+
"withMessages": True,
|
1994
|
+
}
|
1995
|
+
_DEFAULT_FEATURES = {
|
1996
|
+
"c9s_tweet_anatomy_moderator_badge_enabled": True,
|
1997
|
+
"responsive_web_home_pinned_timelines_enabled": True,
|
1998
|
+
"blue_business_profile_image_shape_enabled": True,
|
1999
|
+
"creator_subscriptions_tweet_preview_api_enabled": True,
|
2000
|
+
"freedom_of_speech_not_reach_fetch_enabled": True,
|
2001
|
+
"graphql_is_translatable_rweb_tweet_is_translatable_enabled": True,
|
2002
|
+
"graphql_timeline_v2_bookmark_timeline": True,
|
2003
|
+
"hidden_profile_likes_enabled": True,
|
2004
|
+
"highlights_tweets_tab_ui_enabled": True,
|
2005
|
+
"interactive_text_enabled": True,
|
2006
|
+
"longform_notetweets_consumption_enabled": True,
|
2007
|
+
"longform_notetweets_inline_media_enabled": True,
|
2008
|
+
"longform_notetweets_rich_text_read_enabled": True,
|
2009
|
+
"longform_notetweets_richtext_consumption_enabled": True,
|
2010
|
+
"profile_foundations_tweet_stats_enabled": True,
|
2011
|
+
"profile_foundations_tweet_stats_tweet_frequency": True,
|
2012
|
+
"responsive_web_birdwatch_note_limit_enabled": True,
|
2013
|
+
"responsive_web_edit_tweet_api_enabled": True,
|
2014
|
+
"responsive_web_enhance_cards_enabled": False,
|
2015
|
+
"responsive_web_graphql_exclude_directive_enabled": True,
|
2016
|
+
"responsive_web_graphql_skip_user_profile_image_extensions_enabled": False,
|
2017
|
+
"responsive_web_graphql_timeline_navigation_enabled": True,
|
2018
|
+
"responsive_web_media_download_video_enabled": False,
|
2019
|
+
"responsive_web_text_conversations_enabled": False,
|
2020
|
+
"responsive_web_twitter_article_data_v2_enabled": True,
|
2021
|
+
"responsive_web_twitter_article_tweet_consumption_enabled": False,
|
2022
|
+
"responsive_web_twitter_blue_verified_badge_is_enabled": True,
|
2023
|
+
"rweb_lists_timeline_redesign_enabled": True,
|
2024
|
+
"spaces_2022_h2_clipping": True,
|
2025
|
+
"spaces_2022_h2_spaces_communities": True,
|
2026
|
+
"standardized_nudges_misinfo": True,
|
2027
|
+
"subscriptions_verification_info_verified_since_enabled": True,
|
2028
|
+
"tweet_awards_web_tipping_enabled": False,
|
2029
|
+
"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": True,
|
2030
|
+
"tweetypie_unmention_optimization_enabled": True,
|
2031
|
+
"verified_phone_label_enabled": False,
|
2032
|
+
"vibe_api_enabled": True,
|
2033
|
+
"view_counts_everywhere_api_enabled": True,
|
2034
|
+
"hidden_profile_subscriptions_enabled": True,
|
2035
|
+
"subscriptions_verification_info_is_identity_verified_enabled": True,
|
2036
|
+
}
|
2037
|
+
|
2038
|
+
@classmethod
|
2039
|
+
def _operation_to_url(cls, operation: str) -> tuple[str, str]:
|
2040
|
+
"""
|
2041
|
+
:return: URL and Query ID
|
2042
|
+
"""
|
2043
|
+
query_id = cls._OPERATION_TO_QUERY_ID[operation]
|
2044
|
+
url = f"{cls._GRAPHQL_URL}/{query_id}/{operation}"
|
2045
|
+
return url, query_id
|
2046
|
+
|
2047
|
+
def __init__(self, client: Client):
|
2048
|
+
self._client = client
|
2049
|
+
|
2050
|
+
async def gql_request(
|
2051
|
+
self, method, operation, **kwargs
|
2052
|
+
) -> tuple[requests.Response, dict]:
|
2053
|
+
url, query_id = self._operation_to_url(operation)
|
2054
|
+
|
2055
|
+
if method == "POST":
|
2056
|
+
payload = kwargs["json"] = kwargs.get("json") or {}
|
2057
|
+
payload["queryId"] = query_id
|
2058
|
+
else:
|
2059
|
+
params = kwargs["params"] = kwargs.get("params") or {}
|
2060
|
+
...
|
2061
|
+
|
2062
|
+
response, data = await self._client.request(method, url, **kwargs)
|
2063
|
+
return response, data["data"]
|
2064
|
+
|
2065
|
+
async def user_by_username(self, username: str) -> User | None:
|
2066
|
+
features = self._DEFAULT_FEATURES
|
2067
|
+
variables = self._DEFAULT_VARIABLES
|
2068
|
+
variables["screen_name"] = username
|
2069
|
+
params = {
|
2070
|
+
"variables": variables,
|
2071
|
+
"features": features,
|
2072
|
+
}
|
2073
|
+
response, data = await self.gql_request(
|
2074
|
+
"GET", "UserByScreenName", params=params
|
2075
|
+
)
|
2076
|
+
return User.from_raw_data(data["user"]["result"]) if data else None
|
2077
|
+
|
2078
|
+
async def users_by_ids(
|
2079
|
+
self, user_ids: Iterable[str | int]
|
2080
|
+
) -> dict[int : User | Account]:
|
2081
|
+
features = self._DEFAULT_FEATURES
|
2082
|
+
variables = self._DEFAULT_VARIABLES
|
2083
|
+
variables["userIds"] = list({str(user_id) for user_id in user_ids})
|
2084
|
+
params = {
|
2085
|
+
"variables": variables,
|
2086
|
+
"features": features,
|
2087
|
+
}
|
2088
|
+
response, data = await self.gql_request("GET", "UsersByRestIds", params=params)
|
2089
|
+
|
2090
|
+
users = {}
|
2091
|
+
for user_data in data["users"]:
|
2092
|
+
user = User.from_raw_data(user_data["result"])
|
2093
|
+
users[user.id] = user
|
2094
|
+
if user.id == self._client.account.id:
|
2095
|
+
users[self._client.account.id] = self._client.account
|
2096
|
+
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
|