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.
@@ -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
  Пример настройки клиента:
@@ -1,4 +1,4 @@
1
- twitter/__init__.py,sha256=-CmcPdm1z-OkG8LkJVe75PwdYKBqBfMpD9WdoXcnGuc,732
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=joAB5Zw-Le5E3kOZ-1nb4DPGlTqWYv2Vs6gJ3cwu7is,3175
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=JFPS-9Qae1iY3NfNcywxvWWmRDijaU_Rjs3WaQ00iFA,2071
13
- twitter/client.py,sha256=VZ9cymxz1KMSaqVTTFfCfuC1c5xZHdRUOD0XENSf53w,75555
14
- twitter/enums.py,sha256=-OH6Ibxarq5qt4E2AhkProVawcEyIf5YG_h_G5xiV9Y,270
15
- twitter/errors.py,sha256=oNa0Neos80ZK4-0FBzqgxXonH564qFnoN-kavHalfR4,5274
16
- twitter/models.py,sha256=CrGb3dvA0U4PfPTkUtuprPKXpqkLpM8AR_-De4D3efM,5677
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.10.0b9.dist-info/METADATA,sha256=DUbx5Sw46Hy6B1ZIsQVOnFx3BTjH1qZ0yplMDTJ7njE,13153
22
- tweepy_self-1.10.0b9.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
23
- tweepy_self-1.10.0b9.dist-info/RECORD,,
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
@@ -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
twitter/account.py CHANGED
@@ -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
 
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/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
twitter/client.py CHANGED
@@ -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
twitter/enums.py CHANGED
@@ -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
twitter/errors.py CHANGED
@@ -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)
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"]}