tweepy-self 1.10.0b8__py3-none-any.whl → 1.11.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tweepy-self
3
- Version: 1.10.0b8
3
+ Version: 1.11.0
4
4
  Summary: Twitter (selfbot) for Python!
5
5
  Home-page: https://github.com/alenkimov/tweepy-self
6
6
  Author: Alen
@@ -32,12 +32,19 @@ Description-Content-Type: text/markdown
32
32
 
33
33
  A modern, easy to use, feature-rich, and async ready API wrapper for Twitter's user API written in Python.
34
34
 
35
+ _NEW!_ Менеджер аккаунтов с базой данных:
36
+ - [tweepy-manager](https://github.com/alenkimov/tweepy-manager)
37
+
35
38
  More libraries of the family:
36
39
  - [better-proxy](https://github.com/alenkimov/better_proxy)
37
40
  - [better-web3](https://github.com/alenkimov/better_web3)
38
41
 
39
42
  Отдельное спасибо [Кузнице Ботов](https://t.me/bots_forge) за код для авторизации и разморозки! Подписывайтесь на их Telegram :)
40
43
 
44
+ Похожие библиотеки:
45
+ - [twikit (sync and async)](https://github.com/d60/twikit)
46
+ - [twitter-api-client (sync)](https://github.com/trevorhobenshield/twitter-api-client)
47
+
41
48
  ## Key Features
42
49
  - Modern Pythonic API using async and await.
43
50
  - Prevents user account automation detection.
@@ -84,7 +91,9 @@ logger.enable("twitter")
84
91
  `level="DEBUG"` позволяет увидеть информацию обо всех запросах.
85
92
 
86
93
  ### Аккаунт
87
- После любого взаимодействия с 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
@@ -10,14 +10,14 @@ twitter/account.py,sha256=joAB5Zw-Le5E3kOZ-1nb4DPGlTqWYv2Vs6gJ3cwu7is,3175
10
10
  twitter/base/__init__.py,sha256=Q2ko0HeOS5tiBnDVKxxaZYetwRR3YXJ67ujL3oThGd4,141
11
11
  twitter/base/client.py,sha256=J_iL4ZGfwTbZ2gpjtFCbBxNgt7weJ55EeMGzYsLtjf4,500
12
12
  twitter/base/session.py,sha256=JFPS-9Qae1iY3NfNcywxvWWmRDijaU_Rjs3WaQ00iFA,2071
13
- twitter/client.py,sha256=3eCld11wiZPOKF561RlTcJ_Re-lupvGSPz7MPKrJl1k,75103
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
13
+ twitter/client.py,sha256=WQFcwt9RA4LSgF0iRFLOy5-bJepyb2vLWLMX1iS2k9s,82317
14
+ twitter/enums.py,sha256=RzQnvxEmMwGVTHZ7e3P3kAE6U9btvNZ3cqVMi7y9ItI,299
15
+ twitter/errors.py,sha256=xYuwU8F-_XDGWOLvLUAT_U3sO1ZJOPQ7uOhe-SBo6vY,5610
16
+ twitter/models.py,sha256=HuiZItosr1FibjkHNWZeIsEe9K52o7OBiBuvoNu0Rxc,5905
17
17
  twitter/utils/__init__.py,sha256=usxpfcRQ7zxTTgZ-i425tT7hIz73Pwh9FDj4t6O3dYg,663
18
18
  twitter/utils/file.py,sha256=Sz2KEF9DnL04aOP1XabuMYMMF4VR8dJ_KWMEVvQ666Y,1120
19
19
  twitter/utils/html.py,sha256=nrOJw0vUKfBaHgFaQSQIdXfvfZ8mdu84MU_s46kJTJ4,2087
20
20
  twitter/utils/other.py,sha256=9RIYF2AMdmNKIwClG3jBP7zlvxZPEgYfuHaIiOhURzM,1061
21
- tweepy_self-1.10.0b8.dist-info/METADATA,sha256=gghI1xBSOt1w8QajiIFg6H93LAydql3haQ3tk0YghoU,13153
22
- tweepy_self-1.10.0b8.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
23
- tweepy_self-1.10.0b8.dist-info/RECORD,,
21
+ tweepy_self-1.11.0.dist-info/METADATA,sha256=WKDAyHV76Ewun0b94z7dRHet4iLp8O5YWXWP31euv28,13754
22
+ tweepy_self-1.11.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
23
+ tweepy_self-1.11.0.dist-info/RECORD,,
twitter/__init__.py CHANGED
@@ -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/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
@@ -95,6 +95,8 @@ class Client(BaseHTTPClient):
95
95
  self.auto_relogin = auto_relogin
96
96
  self._update_account_info_on_startup = update_account_info_on_startup
97
97
 
98
+ self.gql = GQLClient(self)
99
+
98
100
  async def __aenter__(self):
99
101
  await self.on_startup()
100
102
  return await super().__aenter__()
@@ -114,16 +116,21 @@ class Client(BaseHTTPClient):
114
116
 
115
117
  if bearer:
116
118
  headers["authorization"] = f"Bearer {self._BEARER_TOKEN}"
117
- # headers["x-twitter-auth-type"] = "OAuth2Session"
118
119
 
119
120
  if auth:
120
121
  if not self.account.auth_token:
121
122
  raise ValueError("No auth_token. Login before")
122
123
 
123
124
  cookies["auth_token"] = self.account.auth_token
125
+ headers["x-twitter-auth-type"] = "OAuth2Session"
124
126
  if self.account.ct0:
125
127
  cookies["ct0"] = self.account.ct0
126
128
  headers["x-csrf-token"] = self.account.ct0
129
+ else:
130
+ if "auth_token" in cookies:
131
+ del cookies["auth_token"]
132
+ if "x-twitter-auth-type" in headers:
133
+ del headers["x-twitter-auth-type"]
127
134
 
128
135
  # fmt: off
129
136
  log_message = (f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
@@ -172,9 +179,9 @@ class Client(BaseHTTPClient):
172
179
  if isinstance(data, dict) and "errors" in data:
173
180
  exc = HTTPException(response, data)
174
181
 
175
- if 141 in exc.api_codes:
182
+ if 141 in exc.api_codes or 37 in exc.api_codes:
176
183
  self.account.status = AccountStatus.SUSPENDED
177
- raise Suspended(exc, self.account)
184
+ raise AccountSuspended(exc, self.account)
178
185
 
179
186
  if 326 in exc.api_codes:
180
187
  for error_data in exc.api_errors:
@@ -184,23 +191,29 @@ class Client(BaseHTTPClient):
184
191
  == "/i/flow/consent_flow"
185
192
  ):
186
193
  self.account.status = AccountStatus.CONSENT_LOCKED
187
- raise ConsentLocked(exc, self.account)
194
+ raise AccountConsentLocked(exc, self.account)
188
195
 
189
196
  self.account.status = AccountStatus.LOCKED
190
- raise Locked(exc, self.account)
197
+ raise AccountLocked(exc, self.account)
191
198
  raise exc
192
199
 
193
200
  return response, data
194
201
 
195
202
  if response.status_code == 400:
196
- raise BadRequest(response, data)
203
+ exc = BadRequest(response, data)
204
+
205
+ if 399 in exc.api_codes:
206
+ self.account.status = AccountStatus.NOT_FOUND
207
+ raise AccountNotFound(exc, self.account)
208
+
209
+ raise exc
197
210
 
198
211
  if response.status_code == 401:
199
212
  exc = Unauthorized(response, data)
200
213
 
201
214
  if 32 in exc.api_codes:
202
215
  self.account.status = AccountStatus.BAD_TOKEN
203
- raise BadToken(exc, self.account)
216
+ raise BadAccountToken(exc, self.account)
204
217
 
205
218
  raise exc
206
219
 
@@ -209,7 +222,7 @@ class Client(BaseHTTPClient):
209
222
 
210
223
  if 64 in exc.api_codes:
211
224
  self.account.status = AccountStatus.SUSPENDED
212
- raise Suspended(exc, self.account)
225
+ raise AccountSuspended(exc, self.account)
213
226
 
214
227
  if 326 in exc.api_codes:
215
228
  for error_data in exc.api_errors:
@@ -218,10 +231,10 @@ class Client(BaseHTTPClient):
218
231
  and error_data.get("bounce_location") == "/i/flow/consent_flow"
219
232
  ):
220
233
  self.account.status = AccountStatus.CONSENT_LOCKED
221
- raise ConsentLocked(exc, self.account)
234
+ raise AccountConsentLocked(exc, self.account)
222
235
 
223
236
  self.account.status = AccountStatus.LOCKED
224
- raise Locked(exc, self.account)
237
+ raise AccountLocked(exc, self.account)
225
238
 
226
239
  raise exc
227
240
 
@@ -261,19 +274,20 @@ class Client(BaseHTTPClient):
261
274
  *,
262
275
  auto_unlock: bool = True,
263
276
  auto_relogin: bool = None,
277
+ rerequest_on_bad_ct0: bool = True,
264
278
  **kwargs,
265
279
  ) -> tuple[requests.Response, Any]:
266
280
  try:
267
281
  return await self._request(method, url, **kwargs)
268
282
 
269
- except Locked:
283
+ except AccountLocked:
270
284
  if not self.capsolver_api_key or not auto_unlock:
271
285
  raise
272
286
 
273
287
  await self.unlock()
274
288
  return await self._request(method, url, **kwargs)
275
289
 
276
- except BadToken:
290
+ except BadAccountToken:
277
291
  if auto_relogin is None:
278
292
  auto_relogin = self.auto_relogin
279
293
  if (
@@ -284,11 +298,17 @@ class Client(BaseHTTPClient):
284
298
  raise
285
299
 
286
300
  await self.relogin()
287
- return await self._request(method, url, **kwargs)
301
+ return await self.request(method, url, auto_relogin=False, **kwargs)
288
302
 
289
303
  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)
304
+ if (
305
+ rerequest_on_bad_ct0
306
+ and 353 in exc.api_codes
307
+ and "ct0" in exc.response.cookies
308
+ ):
309
+ return await self.request(
310
+ method, url, rerequest_on_bad_ct0=False, **kwargs
311
+ )
292
312
  else:
293
313
  raise
294
314
 
@@ -447,9 +467,9 @@ class Client(BaseHTTPClient):
447
467
  "withAuxiliaryUserLabels": False,
448
468
  }
449
469
  params = {
450
- "variables": to_json(variables),
451
- "features": to_json(features),
452
- "fieldToggles": to_json(field_toggles),
470
+ "variables": variables,
471
+ "features": features,
472
+ "fieldToggles": field_toggles,
453
473
  }
454
474
  response, data = await self.request("GET", url, params=params)
455
475
  if not data["data"]:
@@ -915,8 +935,8 @@ class Client(BaseHTTPClient):
915
935
  "responsive_web_enhance_cards_enabled": False,
916
936
  }
917
937
  params = {
918
- "variables": to_json(variables),
919
- "features": to_json(features),
938
+ "variables": variables,
939
+ "features": features,
920
940
  }
921
941
  response, response_json = await self.request("GET", url, params=params)
922
942
 
@@ -1008,8 +1028,8 @@ class Client(BaseHTTPClient):
1008
1028
  "responsive_web_enhance_cards_enabled": False,
1009
1029
  }
1010
1030
  query = {
1011
- "variables": to_json(variables),
1012
- "features": to_json(features),
1031
+ "variables": variables,
1032
+ "features": features,
1013
1033
  }
1014
1034
  response, data = await self.request("GET", url, params=query)
1015
1035
  instructions = data["data"]["threaded_conversation_with_injections_v2"]["instructions"] # type: ignore
@@ -1053,7 +1073,7 @@ class Client(BaseHTTPClient):
1053
1073
  "responsive_web_media_download_video_enabled": False,
1054
1074
  "responsive_web_enhance_cards_enabled": False,
1055
1075
  }
1056
- params = {"variables": to_json(variables), "features": to_json(features)}
1076
+ params = {"variables": variables, "features": features}
1057
1077
  response, data = await self.request("GET", url, params=params)
1058
1078
 
1059
1079
  instructions = data["data"]["user"]["result"]["timeline_v2"]["timeline"][
@@ -1633,12 +1653,12 @@ class Client(BaseHTTPClient):
1633
1653
 
1634
1654
  :return: guest_token
1635
1655
  """
1636
- 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
1656
+ response, data = await self._request(
1657
+ "POST",
1658
+ "https://api.twitter.com/1.1/guest/activate.json",
1659
+ auth=False,
1660
+ )
1661
+ return data["guest_token"]
1642
1662
 
1643
1663
  async def _login(self) -> bool:
1644
1664
  update_backup_code = False
@@ -1658,28 +1678,37 @@ class Client(BaseHTTPClient):
1658
1678
  flow_token, subtasks = await self._login_enter_password(flow_token)
1659
1679
  flow_token, subtasks = await self._account_duplication_check(flow_token)
1660
1680
 
1661
- subtask_ids = {subtask.id for subtask in subtasks}
1662
- if "LoginAcid" in subtask_ids:
1663
- if not self.account.email:
1664
- raise TwitterException(
1665
- f"Failed to login. Task id: LoginAcid. No email!"
1666
- )
1667
-
1668
- try:
1669
- # fmt: off
1670
- flow_token, subtasks = await self._login_acid(flow_token, self.account.email)
1671
- # fmt: on
1672
- except HTTPException as exc:
1673
- if 399 in exc.api_codes:
1674
- logger.warning(
1675
- f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
1676
- f" Bad email!"
1681
+ for subtask in subtasks:
1682
+ if subtask.id == "LoginAcid":
1683
+ if not self.account.email:
1684
+ raise TwitterException(
1685
+ f"Failed to login. Task id: LoginAcid." f" No email!"
1677
1686
  )
1687
+
1688
+ if subtask.primary_text == "Check your email":
1678
1689
  raise TwitterException(
1679
- f"Failed to login. Task id: LoginAcid. Bad email!"
1690
+ f"Failed to login. Task id: LoginAcid."
1691
+ f" Email verification required!"
1692
+ f" No IMAP handler for this version of library :<"
1680
1693
  )
1681
- else:
1682
- raise
1694
+
1695
+ try:
1696
+ # fmt: off
1697
+ flow_token, subtasks = await self._login_acid(flow_token, self.account.email)
1698
+ # fmt: on
1699
+ except HTTPException as exc:
1700
+ if 399 in exc.api_codes:
1701
+ logger.warning(
1702
+ f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
1703
+ f" Bad email!"
1704
+ )
1705
+ raise TwitterException(
1706
+ f"Failed to login. Task id: LoginAcid. Bad email!"
1707
+ )
1708
+ else:
1709
+ raise
1710
+
1711
+ subtask_ids = {subtask.id for subtask in subtasks}
1683
1712
 
1684
1713
  if "LoginTwoFactorAuthChallenge" in subtask_ids:
1685
1714
  if not self.account.totp_secret:
@@ -1917,3 +1946,148 @@ class Client(BaseHTTPClient):
1917
1946
  raise ValueError("Password required to enable TOTP")
1918
1947
 
1919
1948
  await self._enable_totp()
1949
+
1950
+
1951
+ class GQLClient:
1952
+ _GRAPHQL_URL = "https://twitter.com/i/api/graphql"
1953
+ _OPERATION_TO_QUERY_ID = {
1954
+ "CreateRetweet": "ojPdsZsimiJrUGLR1sjUtA",
1955
+ "FavoriteTweet": "lI07N6Otwv1PhnEgXILM7A",
1956
+ "UnfavoriteTweet": "ZYKSe-w7KEslx3JhSIk5LA",
1957
+ "CreateTweet": "v0en1yVV-Ybeek8ClmXwYw",
1958
+ "TweetResultByRestId": "V3vfsYzNEyD9tsf4xoFRgw",
1959
+ "ModerateTweet": "p'jF:GVqCjTcZol0xcBJjw",
1960
+ "DeleteTweet": "VaenaVgh5q5ih7kvyVjgtg",
1961
+ "UserTweets": "V1ze5q3ijDS1VeLwLY0m7g",
1962
+ "TweetDetail": "VWFGPVAGkZMGRKGe3GFFnA",
1963
+ "ProfileSpotlightsQuery": "9zwVLJ48lmVUk8u_Gh9DmA",
1964
+ "Following": "t-BPOrMIduGUJWO_LxcvNQ",
1965
+ "Followers": "3yX7xr2hKjcZYnXt6cU6lQ",
1966
+ "UserByScreenName": "G3KGOASz96M-Qu0nwmGXNg",
1967
+ "UsersByRestIds": "itEhGywpgX9b3GJCzOtSrA",
1968
+ "Viewer": "W62NnYgkgziw9bwyoVht0g",
1969
+ }
1970
+ _DEFAULT_VARIABLES = {
1971
+ "count": 1000,
1972
+ "withSafetyModeUserFields": True,
1973
+ "includePromotedContent": True,
1974
+ "withQuickPromoteEligibilityTweetFields": True,
1975
+ "withVoice": True,
1976
+ "withV2Timeline": True,
1977
+ "withDownvotePerspective": False,
1978
+ "withBirdwatchNotes": True,
1979
+ "withCommunity": True,
1980
+ "withSuperFollowsUserFields": True,
1981
+ "withReactionsMetadata": False,
1982
+ "withReactionsPerspective": False,
1983
+ "withSuperFollowsTweetFields": True,
1984
+ "isMetatagsQuery": False,
1985
+ "withReplays": True,
1986
+ "withClientEventToken": False,
1987
+ "withAttachments": True,
1988
+ "withConversationQueryHighlights": True,
1989
+ "withMessageQueryHighlights": True,
1990
+ "withMessages": True,
1991
+ }
1992
+ _DEFAULT_FEATURES = {
1993
+ "c9s_tweet_anatomy_moderator_badge_enabled": True,
1994
+ "responsive_web_home_pinned_timelines_enabled": True,
1995
+ "blue_business_profile_image_shape_enabled": True,
1996
+ "creator_subscriptions_tweet_preview_api_enabled": True,
1997
+ "freedom_of_speech_not_reach_fetch_enabled": True,
1998
+ "graphql_is_translatable_rweb_tweet_is_translatable_enabled": True,
1999
+ "graphql_timeline_v2_bookmark_timeline": True,
2000
+ "hidden_profile_likes_enabled": True,
2001
+ "highlights_tweets_tab_ui_enabled": True,
2002
+ "interactive_text_enabled": True,
2003
+ "longform_notetweets_consumption_enabled": True,
2004
+ "longform_notetweets_inline_media_enabled": True,
2005
+ "longform_notetweets_rich_text_read_enabled": True,
2006
+ "longform_notetweets_richtext_consumption_enabled": True,
2007
+ "profile_foundations_tweet_stats_enabled": True,
2008
+ "profile_foundations_tweet_stats_tweet_frequency": True,
2009
+ "responsive_web_birdwatch_note_limit_enabled": True,
2010
+ "responsive_web_edit_tweet_api_enabled": True,
2011
+ "responsive_web_enhance_cards_enabled": False,
2012
+ "responsive_web_graphql_exclude_directive_enabled": True,
2013
+ "responsive_web_graphql_skip_user_profile_image_extensions_enabled": False,
2014
+ "responsive_web_graphql_timeline_navigation_enabled": True,
2015
+ "responsive_web_media_download_video_enabled": False,
2016
+ "responsive_web_text_conversations_enabled": False,
2017
+ "responsive_web_twitter_article_data_v2_enabled": True,
2018
+ "responsive_web_twitter_article_tweet_consumption_enabled": False,
2019
+ "responsive_web_twitter_blue_verified_badge_is_enabled": True,
2020
+ "rweb_lists_timeline_redesign_enabled": True,
2021
+ "spaces_2022_h2_clipping": True,
2022
+ "spaces_2022_h2_spaces_communities": True,
2023
+ "standardized_nudges_misinfo": True,
2024
+ "subscriptions_verification_info_verified_since_enabled": True,
2025
+ "tweet_awards_web_tipping_enabled": False,
2026
+ "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": True,
2027
+ "tweetypie_unmention_optimization_enabled": True,
2028
+ "verified_phone_label_enabled": False,
2029
+ "vibe_api_enabled": True,
2030
+ "view_counts_everywhere_api_enabled": True,
2031
+ "hidden_profile_subscriptions_enabled": True,
2032
+ "subscriptions_verification_info_is_identity_verified_enabled": True,
2033
+ }
2034
+
2035
+ @classmethod
2036
+ def _operation_to_url(cls, operation: str) -> tuple[str, str]:
2037
+ """
2038
+ :return: URL and Query ID
2039
+ """
2040
+ query_id = cls._OPERATION_TO_QUERY_ID[operation]
2041
+ url = f"{cls._GRAPHQL_URL}/{query_id}/{operation}"
2042
+ return url, query_id
2043
+
2044
+ def __init__(self, client: Client):
2045
+ self._client = client
2046
+
2047
+ async def gql_request(
2048
+ self, method, operation, **kwargs
2049
+ ) -> tuple[requests.Response, dict]:
2050
+ url, query_id = self._operation_to_url(operation)
2051
+
2052
+ if method == "POST":
2053
+ payload = kwargs["json"] = kwargs.get("json") or {}
2054
+ payload["queryId"] = query_id
2055
+ else:
2056
+ params = kwargs["params"] = kwargs.get("params") or {}
2057
+ ...
2058
+
2059
+ response, data = await self._client.request(method, url, **kwargs)
2060
+ return response, data["data"]
2061
+
2062
+ async def user_by_username(self, username: str) -> User | None:
2063
+ features = self._DEFAULT_FEATURES
2064
+ variables = self._DEFAULT_VARIABLES
2065
+ variables["screen_name"] = username
2066
+ params = {
2067
+ "variables": variables,
2068
+ "features": features,
2069
+ }
2070
+ response, data = await self.gql_request(
2071
+ "GET", "UserByScreenName", params=params
2072
+ )
2073
+ return User.from_raw_data(data["user"]["result"]) if data else None
2074
+
2075
+ async def users_by_ids(
2076
+ self, user_ids: Iterable[str | int]
2077
+ ) -> dict[int : User | Account]:
2078
+ features = self._DEFAULT_FEATURES
2079
+ variables = self._DEFAULT_VARIABLES
2080
+ variables["userIds"] = list({str(user_id) for user_id in user_ids})
2081
+ params = {
2082
+ "variables": variables,
2083
+ "features": features,
2084
+ }
2085
+ response, data = await self.gql_request("GET", "UsersByRestIds", params=params)
2086
+
2087
+ users = {}
2088
+ for user_data in data["users"]:
2089
+ user = User.from_raw_data(user_data["result"])
2090
+ users[user.id] = user
2091
+ if user.id == self._client.account.id:
2092
+ users[self._client.account.id] = self._client.account
2093
+ return users
twitter/enums.py CHANGED
@@ -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"]}