tweepy-self 1.10.0b8__py3-none-any.whl → 1.11.0__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.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"]}