tweepy-self 1.10.0b9__py3-none-any.whl → 1.11.1__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.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
|