tweepy-self 1.10.0b9__tar.gz → 1.11.1__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (23) hide show
  1. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/PKG-INFO +21 -7
  2. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/README.md +20 -6
  3. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/pyproject.toml +1 -1
  4. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/__init__.py +1 -1
  5. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/account.py +1 -1
  6. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/base/session.py +4 -3
  7. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/client.py +235 -67
  8. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/enums.py +1 -0
  9. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/errors.py +15 -8
  10. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/models.py +12 -0
  11. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/_capsolver/__init__.py +0 -0
  12. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/_capsolver/core/__init__.py +0 -0
  13. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/_capsolver/core/base.py +0 -0
  14. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/_capsolver/core/config.py +0 -0
  15. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/_capsolver/core/enum.py +0 -0
  16. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/_capsolver/core/serializer.py +0 -0
  17. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/_capsolver/fun_captcha.py +0 -0
  18. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/base/__init__.py +0 -0
  19. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/base/client.py +0 -0
  20. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/utils/__init__.py +0 -0
  21. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/utils/file.py +0 -0
  22. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/utils/html.py +0 -0
  23. {tweepy_self-1.10.0b9 → tweepy_self-1.11.1}/twitter/utils/other.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tweepy-self
3
- Version: 1.10.0b9
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
- После любого взаимодействия с Twitter устанавливается статус аккаунта:
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
- Для достоверной установки статуса аккаунта используйте метод `Client.establish_status()`
111
+ ### Клиент
112
+ `twitter.Client`
99
113
 
100
- ### Настройка клиента
101
- Класс `twitter.Client` может быть сконфигурирован перед работой. Он принимает в себя следующие параметры:
114
+ #### Настройка
115
+ Клиент может быть сконфигурирован перед работой. Он принимает в себя следующие параметры:
102
116
  - `wait_on_rate_limit` Если включено, то при достижении Rate Limit будет ждать, вместо того, чтобы выбрасывать исключение. Включено по умолчанию.
103
117
  - `capsolver_api_key` API ключ сервиса [CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=m-aE3NeBGZLU). Нужен для автоматической разморозки аккаунта.
104
118
  - `max_unlock_attempts` Максимальное количество попыток разморозки аккаунта. По умолчанию: 5.
105
119
  - `auto_relogin` Если включено, то при невалидном токене (`BAD_TOKEN`) и предоставленных данных для авторизации (имя пользователя, пароль и totp_secret) будет произведен автоматический релогин (замена токена). Включено по умолчанию.
106
- - `update_account_info_on_startup` Если включено, то на старте будет автоматически запрошена информация об аккаунте. Включено по умолчанию.
120
+ - `update_account_info_on_startup` Если включено, то на старте будет автоматически запрошена информация об аккаунте, а также установлен его статус. Включено по умолчанию.
107
121
  - `**session_kwargs` Любые параметры, которые может принимать сессия `curl_cffi.requests.AsyncSession`. Например, можно передать параметр `proxy`.
108
122
 
109
123
  Пример настройки клиента:
@@ -6,12 +6,19 @@
6
6
 
7
7
  A modern, easy to use, feature-rich, and async ready API wrapper for Twitter's user API written in Python.
8
8
 
9
+ _NEW!_ Менеджер аккаунтов с базой данных:
10
+ - [tweepy-manager](https://github.com/alenkimov/tweepy-manager)
11
+
9
12
  More libraries of the family:
10
13
  - [better-proxy](https://github.com/alenkimov/better_proxy)
11
14
  - [better-web3](https://github.com/alenkimov/better_web3)
12
15
 
13
16
  Отдельное спасибо [Кузнице Ботов](https://t.me/bots_forge) за код для авторизации и разморозки! Подписывайтесь на их Telegram :)
14
17
 
18
+ Похожие библиотеки:
19
+ - [twikit (sync and async)](https://github.com/d60/twikit)
20
+ - [twitter-api-client (sync)](https://github.com/trevorhobenshield/twitter-api-client)
21
+
15
22
  ## Key Features
16
23
  - Modern Pythonic API using async and await.
17
24
  - Prevents user account automation detection.
@@ -58,7 +65,9 @@ logger.enable("twitter")
58
65
  `level="DEBUG"` позволяет увидеть информацию обо всех запросах.
59
66
 
60
67
  ### Аккаунт
61
- После любого взаимодействия с Twitter устанавливается статус аккаунта:
68
+ `twitter.Account`
69
+
70
+ #### Статусы аккаунта
62
71
  - `UNKNOWN` - Статус аккаунта не установлен. Это статус по умолчанию.
63
72
  - `BAD_TOKEN` - Неверный или мертвый токен.
64
73
  - `SUSPENDED` - Действие учетной записи приостановлено. Тем не менее возможен запрос данных, а также авторизация через OAuth и OAuth2.
@@ -66,18 +75,23 @@ logger.enable("twitter")
66
75
  - `CONSENT_LOCKED` - Учетная запись заморожена (лок). Условия для разморозки неизвестны.
67
76
  - `GOOD` - Аккаунт в порядке.
68
77
 
78
+ Метод `Client.establish_status()` устанавливает статус аккаунта.
79
+ Также статус аккаунта может изменить любое взаимодействие с Twitter.
80
+ Поэтому, во время работы может внезапно быть вызвано исключение семейства `twitter.errors.BadAccount`.
81
+
69
82
  Не каждое взаимодействие с Twitter достоверно определяет статус аккаунта.
70
- Например, простой запрос данных об аккаунте честно вернет данные, даже если действие вашей учетной записи приостановлено.
83
+ Например, простой запрос данных об аккаунте честно вернет данные и не изменит статус, даже если действие вашей учетной записи приостановлено (`SUSPENDED`).
71
84
 
72
- Для достоверной установки статуса аккаунта используйте метод `Client.establish_status()`
85
+ ### Клиент
86
+ `twitter.Client`
73
87
 
74
- ### Настройка клиента
75
- Класс `twitter.Client` может быть сконфигурирован перед работой. Он принимает в себя следующие параметры:
88
+ #### Настройка
89
+ Клиент может быть сконфигурирован перед работой. Он принимает в себя следующие параметры:
76
90
  - `wait_on_rate_limit` Если включено, то при достижении Rate Limit будет ждать, вместо того, чтобы выбрасывать исключение. Включено по умолчанию.
77
91
  - `capsolver_api_key` API ключ сервиса [CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=m-aE3NeBGZLU). Нужен для автоматической разморозки аккаунта.
78
92
  - `max_unlock_attempts` Максимальное количество попыток разморозки аккаунта. По умолчанию: 5.
79
93
  - `auto_relogin` Если включено, то при невалидном токене (`BAD_TOKEN`) и предоставленных данных для авторизации (имя пользователя, пароль и totp_secret) будет произведен автоматический релогин (замена токена). Включено по умолчанию.
80
- - `update_account_info_on_startup` Если включено, то на старте будет автоматически запрошена информация об аккаунте. Включено по умолчанию.
94
+ - `update_account_info_on_startup` Если включено, то на старте будет автоматически запрошена информация об аккаунте, а также установлен его статус. Включено по умолчанию.
81
95
  - `**session_kwargs` Любые параметры, которые может принимать сессия `curl_cffi.requests.AsyncSession`. Например, можно передать параметр `proxy`.
82
96
 
83
97
  Пример настройки клиента:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "tweepy-self"
3
- version = "1.10.0.b9"
3
+ version = "1.11.1"
4
4
  description = "Twitter (selfbot) for Python!"
5
5
  authors = ["Alen <alen.kimov@gmail.com>"]
6
6
  readme = "README.md"
@@ -2,7 +2,7 @@
2
2
  Twitter API Wrapper
3
3
  ~~~~~~~~~~~~~~~~~~~
4
4
 
5
- A basic wrapper for the Twitter user API.
5
+ A Python library for interacting with the Twitter API.
6
6
  """
7
7
 
8
8
  from .client import Client
@@ -50,7 +50,7 @@ class Account(User):
50
50
 
51
51
  def get_totp_code(self) -> str | None:
52
52
  if not self.totp_secret:
53
- raise ValueError("No key2fa")
53
+ raise ValueError("No totp_secret")
54
54
 
55
55
  return str(pyotp.TOTP(self.totp_secret).now())
56
56
 
@@ -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/120.0.0.0 Safari/537.36",
18
- "sec-ch-ua": '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
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": "same-origin",
24
+ "sec-fetch-site": "ssame-site",
24
25
  "connection": "keep-alive",
25
26
  }
26
27
  DEFAULT_IMPERSONATE = requests.BrowserType.chrome120
@@ -22,12 +22,12 @@ from .errors import (
22
22
  RateLimited,
23
23
  ServerError,
24
24
  BadAccount,
25
- BadToken,
26
- Locked,
27
- ConsentLocked,
28
- Suspended,
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": "twitter.com",
45
- "origin": "https://twitter.com",
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://twitter.com/i/api/graphql"
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": "v0en1yVV-Ybeek8ClmXwYw",
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": "W62NnYgkgziw9bwyoVht0g",
65
+ "Viewer": "-876iyxD1O_0X0BqeykjZA",
66
66
  }
67
- _CAPTCHA_URL = "https://twitter.com/account/access"
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") or {}
113
- headers = kwargs["headers"] = kwargs.get("headers") or {}
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=".twitter.com"):
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 Suspended(exc, self.account)
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 ConsentLocked(exc, self.account)
193
+ raise AccountConsentLocked(exc, self.account)
188
194
 
189
195
  self.account.status = AccountStatus.LOCKED
190
- raise Locked(exc, self.account)
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
- raise BadRequest(response, data)
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 BadToken(exc, self.account)
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 Suspended(exc, self.account)
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 ConsentLocked(exc, self.account)
233
+ raise AccountConsentLocked(exc, self.account)
222
234
 
223
235
  self.account.status = AccountStatus.LOCKED
224
- raise Locked(exc, self.account)
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 Locked:
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 BadToken:
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._request(method, url, **kwargs)
300
+ return await self.request(method, url, auto_relogin=False, **kwargs)
288
301
 
289
302
  except Forbidden as exc:
290
- if 353 in exc.api_codes and "ct0" in exc.response.cookies:
291
- return await self.request(method, url, **kwargs)
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://twitter.com/i/api/2/oauth2/authorize"
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://twitter.com/i/api/2/oauth2/authorize",
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.twitter.com/oauth/authenticate"
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.twitter.com/oauth/authorize"
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://twitter.com/i/api/1.1/account/settings.json"
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": to_json(variables),
451
- "features": to_json(features),
452
- "fieldToggles": to_json(field_toggles),
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.twitter.com/1.1/media/upload.json"
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://twitter.com/i/api/1.1/friendships/{action}.json"
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.twitter.com/1.1/account/pin_tweet.json"
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.twitter.com/v2/capi/passthrough/1"
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": to_json(variables),
919
- "features": to_json(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": to_json(variables),
1012
- "features": to_json(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": to_json(variables), "features": to_json(features)}
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.twitter.com/1.1/account/update_profile_{type}.json"
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://twitter.com/i/api/1.1/account/settings.json"
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://twitter.com/i/api/i/account/change_password.json"
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://twitter.com/i/api/1.1/account/update_profile.json"
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://twitter.com/i/api/1.1/account/update_profile.json"
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://twitter.com/i/api/1.1/account/update_profile.json"
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.twitter.com/1.1/direct_messages/events/new.json"
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.twitter.com/2/dm_conversations/{conversation_id}/messages"
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://twitter.com/i/api/1.1/dm/inbox_initial_state.json"
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://twitter.com/?lang=en":
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.twitter.com/1.1/account/backup_code.json"
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.twitter.com/1.1/onboarding/task.json"
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
- url = "https://twitter.com"
1637
- response = await self._session.request("GET", url)
1638
- # TODO Если в сессии есть рабочий auth_token, то не вернет нужную страницу.
1639
- # Поэтому нужно очищать сессию перед вызовом этого метода.
1640
- guest_token = re.search(r"gt\s?=\s?\d+", response.text)[0].split("=")[1]
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://twitter.com/i/api/1.1/strato/column/User/{self.account.id}/account-security/twoFactorAuthSettings2"
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
@@ -8,6 +8,7 @@ class AccountStatus(enum.StrEnum):
8
8
  LOCKED = "LOCKED"
9
9
  CONSENT_LOCKED = "CONSENT_LOCKED"
10
10
  GOOD = "GOOD"
11
+ NOT_FOUND = "NOT_FOUND"
11
12
 
12
13
  def __str__(self):
13
14
  return self.value
@@ -13,10 +13,11 @@ __all__ = [
13
13
  "RateLimited",
14
14
  "ServerError",
15
15
  "BadAccount",
16
- "BadToken",
17
- "Locked",
18
- "ConsentLocked",
19
- "Suspended",
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 BadToken(BadAccount):
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 Locked(BadAccount):
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 ConsentLocked(BadAccount):
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 Suspended(BadAccount):
176
+ class AccountSuspended(BadAccount):
176
177
  def __init__(self, http_exception: "HTTPException", account: Account):
177
178
  exception_message = f"Twitter account is suspended."
178
179
  super().__init__(http_exception, account, exception_message)
180
+
181
+
182
+ class AccountNotFound(BadAccount):
183
+ def __init__(self, http_exception: "HTTPException", account: Account):
184
+ exception_message = f"Twitter account not found or deleted."
185
+ super().__init__(http_exception, account, exception_message)
@@ -26,6 +26,9 @@ class Media(BaseModel):
26
26
  def __str__(self):
27
27
  return str(self.id)
28
28
 
29
+ def __hash__(self):
30
+ return hash(self.id)
31
+
29
32
 
30
33
  class User(BaseModel):
31
34
  # fmt: off
@@ -46,6 +49,9 @@ class User(BaseModel):
46
49
  def __repr__(self):
47
50
  return f"{self.__class__.__name__}(id={self.id}, username={self.username})"
48
51
 
52
+ def __hash__(self):
53
+ return hash(self.id)
54
+
49
55
  @classmethod
50
56
  def from_raw_data(cls, data: dict):
51
57
  legacy = data["legacy"]
@@ -105,6 +111,9 @@ class Tweet(BaseModel):
105
111
  def __repr__(self):
106
112
  return f"{self.__class__.__name__}(id={self.id}, user_id={self.user.id})"
107
113
 
114
+ def __hash__(self):
115
+ return hash(self.id)
116
+
108
117
  @property
109
118
  def short_text(self) -> str:
110
119
  return f"{self.text[:32]}..." if len(self.text) > 16 else self.text
@@ -162,6 +171,9 @@ class Subtask(BaseModel):
162
171
  detail_text: Optional[str] = None
163
172
  raw_data: dict
164
173
 
174
+ def __hash__(self):
175
+ return hash(self.id)
176
+
165
177
  @classmethod
166
178
  def from_raw_data(cls, data: dict) -> "Subtask":
167
179
  task = {"id": data["subtask_id"]}