tweepy-self 1.10.0b1__py3-none-any.whl → 1.10.0b4__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.
@@ -0,0 +1,303 @@
1
+ Metadata-Version: 2.1
2
+ Name: tweepy-self
3
+ Version: 1.10.0b4
4
+ Summary: Twitter (selfbot) for Python!
5
+ Home-page: https://github.com/alenkimov/tweepy-self
6
+ Author: Alen
7
+ Author-email: alen.kimov@gmail.com
8
+ Requires-Python: >=3.11,<4.0
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Dist: aiohttp (>=3.9,<4.0)
13
+ Requires-Dist: beautifulsoup4 (>=4,<5)
14
+ Requires-Dist: better-proxy (>=1.1,<2.0)
15
+ Requires-Dist: curl_cffi (==0.6.2)
16
+ Requires-Dist: loguru (>=0.7,<0.8)
17
+ Requires-Dist: lxml (>=5,<6)
18
+ Requires-Dist: pydantic (>=2,<3)
19
+ Requires-Dist: pyotp (>=2,<3)
20
+ Requires-Dist: requests (>=2,<3)
21
+ Requires-Dist: tenacity (>=8,<9)
22
+ Requires-Dist: yarl (>=1,<2)
23
+ Project-URL: Repository, https://github.com/alenkimov/tweepy-self
24
+ Project-URL: Source, https://github.com/alenkimov/tweepy-self
25
+ Description-Content-Type: text/markdown
26
+
27
+ # Tweepy-self
28
+ [![Telegram channel](https://img.shields.io/endpoint?url=https://runkit.io/damiankrawczyk/telegram-badge/branches/master?url=https://t.me/cum_insider)](https://t.me/cum_insider)
29
+ [![PyPI version info](https://img.shields.io/pypi/v/tweepy-self.svg)](https://pypi.python.org/pypi/tweepy-self)
30
+ [![PyPI supported Python versions](https://img.shields.io/pypi/pyversions/tweepy-self.svg)](https://pypi.python.org/pypi/tweepy-self)
31
+ [![PyPI downloads per month](https://img.shields.io/pypi/dm/tweepy-self.svg)](https://pypi.python.org/pypi/tweepy-self)
32
+
33
+ A modern, easy to use, feature-rich, and async ready API wrapper for Twitter's user API written in Python.
34
+
35
+ More libraries of the family:
36
+ - [better-proxy](https://github.com/alenkimov/better_proxy)
37
+ - [better-web3](https://github.com/alenkimov/better_web3)
38
+
39
+ Отдельное спасибо [Кузнице Ботов](https://t.me/bots_forge) за код для авторизации и разморозки! Подписывайтесь на их Telegram :)
40
+
41
+ ## Key Features
42
+ - Modern Pythonic API using async and await.
43
+ - Prevents user account automation detection.
44
+
45
+ ## Installing
46
+ ```bash
47
+ pip install tweepy-self
48
+ ```
49
+
50
+ ## Example
51
+ ```python
52
+ import asyncio
53
+ import twitter
54
+
55
+ twitter_account = twitter.Account(auth_token="auth_token")
56
+
57
+ async def main():
58
+ async with twitter.Client(twitter_account) as twitter_client:
59
+ print(f"Logged in as @{twitter_account.username} (id={twitter_account.id})")
60
+ tweet = await twitter_client.tweet("Hello tweepy-self! <3")
61
+ print(tweet)
62
+
63
+ if __name__ == "__main__":
64
+ asyncio.run(main())
65
+ ```
66
+
67
+ ## Документация
68
+ ### Некоторые истины
69
+ Имена пользователей нужно передавать БЕЗ знака `@`.
70
+ Чтобы наверняка убрать этот знак можно передать имя пользователя в функцию `twitter.utils.remove_at_sign()`
71
+
72
+ Automating user accounts is against the Twitter ToS. This library is a proof of concept and I cannot recommend using it. Do so at your own risk
73
+
74
+ ### Как включить логирование
75
+ ```python
76
+ import sys
77
+ from loguru import logger
78
+
79
+ logger.remove()
80
+ logger.add(sys.stdout, level="INFO")
81
+ logger.enable("twitter")
82
+ ```
83
+
84
+ `level="DEBUG"` позволяет увидеть информацию обо всех запросах.
85
+
86
+ ### Аккаунт
87
+ После любого взаимодействия с Twitter устанавливается статус аккаунта:
88
+ - `UNKNOWN` - Статус аккаунта не установлен. Это статус по умолчанию.
89
+ - `BAD_TOKEN` - Неверный или мертвый токен.
90
+ - `SUSPENDED` - Действие учетной записи приостановлено. Тем не менее возможен запрос данных, а также авторизация через OAuth и OAuth2.
91
+ - `LOCKED` - Учетная запись заморожена (лок). Для разморозки (анлок) требуется прохождение капчи (funcaptcha).
92
+ - `CONSENT_LOCKED` - Учетная запись заморожена (лок). Условия для разморозки неизвестны.
93
+ - `GOOD` - Аккаунт в порядке.
94
+
95
+ Не каждое взаимодействие с Twitter достоверно определяет статус аккаунта.
96
+ Например, простой запрос данных об аккаунте честно вернет данные, даже если действие вашей учетной записи приостановлено.
97
+
98
+ Для достоверной установки статуса аккаунта используйте метод `Client.establish_status()`
99
+
100
+ ### Настройка клиента
101
+ Класс `twitter.Client` может быть сконфигурирован перед работой. Он принимает в себя следующие параметры:
102
+ - `wait_on_rate_limit` Если включено, то при достижении Rate Limit будет ждать, вместо того, чтобы выбрасывать исключение. Включено по умолчанию.
103
+ - `capsolver_api_key` API ключ сервиса [CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=m-aE3NeBGZLU). Нужен для автоматической разморозки аккаунта.
104
+ - `max_unlock_attempts` Максимальное количество попыток разморозки аккаунта. По умолчанию: 5.
105
+ - `auto_relogin` Если включено, то при невалидном токене (`BAD_TOKEN`) и предоставленных данных для авторизации (имя пользователя, пароль и totp_secret) будет произведен автоматический релогин (замена токена). Включено по умолчанию.
106
+ - `update_account_info_on_startup` Если включено, то на старте будет автоматически запрошена информация об аккаунте. Включено по умолчанию.
107
+ - `**session_kwargs` Любые параметры, которые может принимать сессия `curl_cffi.requests.AsyncSession`. Например, можно передать параметр `proxy`.
108
+
109
+ Пример настройки клиента:
110
+ ```python
111
+ async with twitter.Client(
112
+ twitter_account,
113
+ capsolver_api_key="CAP-00000000000000000000000000000000",
114
+ proxy="http://login:password@ip:port", # Можно передавать в любом формате, так как используется библиотека better_proxy
115
+ ) as twitter_client:
116
+ ...
117
+ ```
118
+
119
+ ### Доступные методы
120
+ Список всех методов.
121
+
122
+ #### Запрос информации о собственном аккаунте
123
+ ```python
124
+ twitter_client.update_account_info()
125
+ print(twitter_client.account)
126
+ ```
127
+
128
+ #### Запрос пользователя по username или по ID
129
+
130
+ ```python
131
+ bro = twitter_client.request_user_by_username(bro_username)
132
+ bro = twitter_client.request_user_by_id(bro_id)
133
+ bros = twitter_client.request_users_by_ids([bro1_id, bro2_id, ...])
134
+ ```
135
+
136
+ #### Загрузка изображения на сервер, смена аватарки и баннера
137
+ ```python
138
+ image = open("image.png", "rb").read()
139
+ media = await twitter_client.upload_image(image)
140
+ avatar_image_url = await twitter_client.update_profile_avatar(media.id)
141
+ banner_image_url = await twitter_client.update_profile_banner(media.id)
142
+ ```
143
+
144
+ #### Изменения данных профиля
145
+ ```python
146
+ await twitter_client.update_birthdate(day=1, month=12, year=2000)
147
+ await twitter_client.update_profile( # Locks account!
148
+ name="New Name",
149
+ description="New description",
150
+ location="New York",
151
+ website="https://github.com/alenkimov/tweepy-self",
152
+ )
153
+ ```
154
+
155
+ #### Включение TOTP (2FA)
156
+ ```python
157
+ if await twitter_client.totp_is_enabled():
158
+ print(f"TOTP уже включен.")
159
+ return
160
+
161
+ await twitter_client.enable_totp()
162
+ ```
163
+
164
+ #### Логин, если включен TOTP (2F)
165
+ ```python
166
+ import twitter
167
+
168
+ twitter_account = twitter.Account(auth_token="...", username="...", password="...", totp_secret="...")
169
+ await twitter_client.login()
170
+ print(f"Logged in! New auth_token: {twitter_account.auth_token}")
171
+ ```
172
+
173
+ #### Смена имени пользователя и пароля
174
+ ```python
175
+ twitter_account = twitter.Account("auth_token", password="password")
176
+ ...
177
+ await twitter_client.change_username("new_username")
178
+ await twitter_client.request_user()
179
+ print(f"New username: {twitter_account.username}")
180
+
181
+ await twitter_client.change_password("new_password")
182
+ print(f"New password: {twitter_account.password}")
183
+ print(f"New auth_token: {twitter_account.auth_token}")
184
+ ```
185
+
186
+ #### Авторизация с OAuth
187
+ ```python
188
+ auth_code = await twitter_client.oauth(oauth_token, **oauth_params)
189
+ ```
190
+
191
+ #### Авторизация с OAuth2
192
+ ```python
193
+ # Изучите запросы сервиса и найдите подобные данные для авторизации (привязки):
194
+ oauth2_data = {
195
+ 'response_type': 'code',
196
+ 'client_id': 'TjFVQm52ZDFGWEtNT0tKaktaSWU6MTpjaQ',
197
+ 'redirect_uri': 'https://waitlist.lens.xyz/tw/',
198
+ 'scope': 'users.read tweet.read offline.access',
199
+ 'state': 'state', # Может быть как статичным, так и динамическим.
200
+ 'code_challenge': 'challenge',
201
+ 'code_challenge_method': 'plain'
202
+ }
203
+
204
+ auth_code = await twitter_client.oauth2(**oauth2_data)
205
+ # Передайте код авторизации (привязки) сервису.
206
+ # Сервис также может потребовать state, если он динамический.
207
+ ```
208
+
209
+ #### Отправка сообщения:
210
+ ```python
211
+ bro = await twitter_client.request_user("bro_username")
212
+ await twitter_client.send_message(bro.id, "I love you!")
213
+ ```
214
+
215
+ #### Запрос входящих сообщений:
216
+ ```python
217
+ messages = await twitter_client.request_messages()
218
+ for message in messages:
219
+ message_data = message["message_data"]
220
+ recipient_id = message_data["recipient_id"]
221
+ sender_id = message_data["sender_id"]
222
+ text = message_data["text"]
223
+ print(f"[id {sender_id}] -> [id {recipient_id}]: {text}")
224
+ ```
225
+
226
+ Так как мне почти не приходилось работать с сообщениями, я еще не сделал для этого удобных моделей.
227
+ Поэтому приходится работать со словарем.
228
+
229
+ #### Пост (твит)
230
+ ```python
231
+ tweet = await twitter_client.tweet("I love you tweepy-self! <3")
232
+ print(f"Любовь выражена! Tweet id: {tweet.id}")
233
+ ```
234
+
235
+ #### Лайк, репост (ретвит), коммент (реплай)
236
+ ```python
237
+ # Лайк
238
+ print(f"Tweet {tweet_id} is liked: {await twitter_client.like(tweet_id)}")
239
+
240
+ # Репост (ретвит)
241
+ print(f"Tweet {tweet_id} is retweeted. Tweet id: {await twitter_client.repost(tweet_id)}")
242
+
243
+ # Коммент (реплай)
244
+ print(f"Tweet {tweet_id} is replied. Reply id: {await twitter_client.reply(tweet_id, 'tem razão')}")
245
+ ```
246
+
247
+ #### Цитата
248
+ ```python
249
+ tweet_url = 'https://twitter.com/CreamIce_Cone/status/1691735090529976489'
250
+ # Цитата (Quote tweet)
251
+ quote_tweet_id = await twitter_client.quote(tweet_url, 'oh....')
252
+ print(f"Quoted! Tweet id: {quote_tweet_id}")
253
+ ```
254
+
255
+ #### Подписка и отписка
256
+ ```python
257
+ # Подписываемся на Илона Маска
258
+ print(f"@{elonmusk.username} is followed: {await twitter_client.follow(elonmusk.id)}")
259
+
260
+ # Отписываемся от Илона Маска
261
+ print(f"@{elonmusk.username} is unfollowed: {await twitter_client.unfollow(elonmusk.id)}")
262
+ ```
263
+
264
+ #### Закрепление твита
265
+ ```python
266
+ pinned = await twitter_client.pin_tweet(tweet_id)
267
+ print(f"Tweet is pined: {pinned}")
268
+ ```
269
+
270
+ #### Запрос своих и чужих подписчиков
271
+ ```python
272
+
273
+ followers = await twitter_client.request_followers()
274
+ print("Твои подписчики:")
275
+ for user in followers:
276
+ print(user)
277
+
278
+ followings = await twitter_client.request_followings()
279
+ print(f"Ты подписан на:")
280
+ for user in followings:
281
+ print(user)
282
+
283
+ bro_followers = await twitter_client.request_followers(bro_id)
284
+ print(f"Подписчики твоего бро (id={bro_id}):")
285
+ for user in bro_followers:
286
+ print(user)
287
+
288
+ bro_followings = await twitter_client.request_followings(bro_id)
289
+ print(f"На твоего бро (id={bro_id}) подписаны:")
290
+ for user in bro_followings:
291
+ print(user)
292
+ ```
293
+
294
+ #### Голосование
295
+ ```python
296
+ vote_data = await twitter_client.vote(tweet_id, card_id, choice_number)
297
+ votes_count = vote_data["card"]["binding_values"]["choice1_count"]["string_value"]
298
+ print(f"Votes: {votes_count}")
299
+ ```
300
+
301
+ Так как мне почти не приходилось работать с голосованиями, я еще не сделал для этого удобных моделей.
302
+ Поэтому приходится работать со словарем.
303
+
@@ -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=CySQ-hTFiPGFKhPBNw4nn_xnO5hdpjmXK90QpSEzRG4,66878
13
+ twitter/client.py,sha256=PNAco0kak2q3E7iinSQePa7LKAMcxgbbgH0NtIo8-Jk,69923
14
14
  twitter/enums.py,sha256=-OH6Ibxarq5qt4E2AhkProVawcEyIf5YG_h_G5xiV9Y,270
15
15
  twitter/errors.py,sha256=oNa0Neos80ZK4-0FBzqgxXonH564qFnoN-kavHalfR4,5274
16
16
  twitter/models.py,sha256=7yObMPUUEwJEbraHzFwmUKd91UhR2-zyfJTm4xIqrSQ,4834
17
17
  twitter/utils/__init__.py,sha256=usxpfcRQ7zxTTgZ-i425tT7hIz73Pwh9FDj4t6O3dYg,663
18
18
  twitter/utils/file.py,sha256=Sz2KEF9DnL04aOP1XabuMYMMF4VR8dJ_KWMEVvQ666Y,1120
19
- twitter/utils/html.py,sha256=hVtIRFI2yRAdWEaShFNBG-_ZWxd16og8i8OVDnFy5Hc,1971
19
+ twitter/utils/html.py,sha256=nrOJw0vUKfBaHgFaQSQIdXfvfZ8mdu84MU_s46kJTJ4,2087
20
20
  twitter/utils/other.py,sha256=9RIYF2AMdmNKIwClG3jBP7zlvxZPEgYfuHaIiOhURzM,1061
21
- tweepy_self-1.10.0b1.dist-info/METADATA,sha256=FRxYUeZHlrxUB9Jr_nbxlhEkLc1pjO-ik_XiShkwARg,9438
22
- tweepy_self-1.10.0b1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
23
- tweepy_self-1.10.0b1.dist-info/RECORD,,
21
+ tweepy_self-1.10.0b4.dist-info/METADATA,sha256=orS4YvArvrgjjz3BNRap3vua063lo2S7y8ReSGtCgdU,13152
22
+ tweepy_self-1.10.0b4.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
23
+ tweepy_self-1.10.0b4.dist-info/RECORD,,
twitter/client.py CHANGED
@@ -32,7 +32,6 @@ from .base import BaseHTTPClient
32
32
  from .account import Account, AccountStatus
33
33
  from .models import User, Tweet, Media
34
34
  from .utils import (
35
- remove_at_sign,
36
35
  parse_oauth_html,
37
36
  parse_unlock_html,
38
37
  tweets_data_from_instructions,
@@ -52,7 +51,7 @@ class Client(BaseHTTPClient):
52
51
  "CreateRetweet": "ojPdsZsimiJrUGLR1sjUtA",
53
52
  "FavoriteTweet": "lI07N6Otwv1PhnEgXILM7A",
54
53
  "UnfavoriteTweet": "ZYKSe-w7KEslx3JhSIk5LA",
55
- "CreateTweet": "SoVnbfCycZ7fERGCwpZkYA",
54
+ "CreateTweet": "v0en1yVV-Ybeek8ClmXwYw",
56
55
  "TweetResultByRestId": "V3vfsYzNEyD9tsf4xoFRgw",
57
56
  "ModerateTweet": "p'jF:GVqCjTcZol0xcBJjw",
58
57
  "DeleteTweet": "VaenaVgh5q5ih7kvyVjgtg",
@@ -85,7 +84,7 @@ class Client(BaseHTTPClient):
85
84
  capsolver_api_key: str = None,
86
85
  max_unlock_attempts: int = 5,
87
86
  auto_relogin: bool = True,
88
- request_self_on_startup: bool = True,
87
+ update_account_info_on_startup: bool = True,
89
88
  **session_kwargs,
90
89
  ):
91
90
  super().__init__(**session_kwargs)
@@ -94,7 +93,7 @@ class Client(BaseHTTPClient):
94
93
  self.capsolver_api_key = capsolver_api_key
95
94
  self.max_unlock_attempts = max_unlock_attempts
96
95
  self.auto_relogin = auto_relogin
97
- self.request_self_on_startup = request_self_on_startup
96
+ self._update_account_info_on_startup = update_account_info_on_startup
98
97
 
99
98
  async def __aenter__(self):
100
99
  await self.on_startup()
@@ -159,7 +158,7 @@ class Client(BaseHTTPClient):
159
158
  auth_token = self._session.cookies.get("auth_token")
160
159
  if auth_token and auth_token != self.account.auth_token:
161
160
  self.account.auth_token = auth_token
162
- logger.info(
161
+ logger.warning(
163
162
  f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
164
163
  f" Requested new auth_token!"
165
164
  )
@@ -239,7 +238,7 @@ class Client(BaseHTTPClient):
239
238
  reset_time = int(response.headers["x-rate-limit-reset"])
240
239
  sleep_time = reset_time - int(time()) + 1
241
240
  if sleep_time > 0:
242
- logger.info(
241
+ logger.warning(
243
242
  f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
244
243
  f"Rate limited! Sleep time: {sleep_time} sec."
245
244
  )
@@ -290,13 +289,13 @@ class Client(BaseHTTPClient):
290
289
 
291
290
  except Forbidden as exc:
292
291
  if 353 in exc.api_codes and "ct0" in exc.response.cookies:
293
- return await self._request(method, url, **kwargs)
292
+ return await self.request(method, url, **kwargs)
294
293
  else:
295
294
  raise
296
295
 
297
296
  async def on_startup(self):
298
- if self.request_self_on_startup:
299
- await self.request_user()
297
+ if self._update_account_info_on_startup:
298
+ await self.update_account_info()
300
299
 
301
300
  async def _request_oauth2_auth_code(
302
301
  self,
@@ -421,14 +420,13 @@ class Client(BaseHTTPClient):
421
420
 
422
421
  return authenticity_token, redirect_url
423
422
 
424
- async def request_and_set_username(self):
423
+ async def _update_account_username(self):
425
424
  url = "https://twitter.com/i/api/1.1/account/settings.json"
426
425
  response, response_json = await self.request("POST", url)
427
426
  self.account.username = response_json["screen_name"]
428
427
 
429
- async def _request_user(self, username: str) -> User:
428
+ async def _request_user_by_username(self, username: str) -> User | None:
430
429
  url, query_id = self._action_to_url("UserByScreenName")
431
- username = remove_at_sign(username)
432
430
  variables = {
433
431
  "screen_name": username,
434
432
  "withSafetyModeUserFields": True,
@@ -454,31 +452,76 @@ class Client(BaseHTTPClient):
454
452
  "fieldToggles": to_json(field_toggles),
455
453
  }
456
454
  response, data = await self.request("GET", url, params=params)
455
+ if not data["data"]:
456
+ return None
457
457
  return User.from_raw_data(data["data"]["user"]["result"])
458
458
 
459
- async def request_user(
460
- self, *, username: str = None, user_id: int | str = None
461
- ) -> User | Account:
462
- if username and user_id:
463
- raise ValueError("Specify username or user_id, not both.")
464
-
465
- if user_id:
466
- users = await self.request_users((user_id,))
467
- user = users[user_id]
468
- else:
469
- if not username:
470
- if not self.account.username:
471
- await self.request_and_set_username()
472
- username = self.account.username
459
+ async def request_user_by_username(self, username: str) -> User | Account | None:
460
+ """
461
+ :param username: Имя пользователя без знака `@`
462
+ :return: Пользователь, если существует, иначе None. Или собственный аккаунт, если совпадает имя пользователя.
463
+ """
464
+ if not self.account.username:
465
+ await self.update_account_info()
473
466
 
474
- user = await self._request_user(username)
467
+ user = await self._request_user_by_username(username)
475
468
 
476
- if self.account.username == user.username:
469
+ if user and user.id == self.account.id:
477
470
  self.account.update(**user.model_dump())
478
- user = self.account
471
+ return self.account
479
472
 
480
473
  return user
481
474
 
475
+ async def _request_users_by_ids(
476
+ self, user_ids: Iterable[str | int]
477
+ ) -> dict[int : User | Account]:
478
+ url, query_id = self._action_to_url("UsersByRestIds")
479
+ variables = {"userIds": list({str(user_id) for user_id in user_ids})}
480
+ features = {
481
+ "responsive_web_graphql_exclude_directive_enabled": True,
482
+ "responsive_web_graphql_skip_user_profile_image_extensions_enabled": False,
483
+ "responsive_web_graphql_timeline_navigation_enabled": True,
484
+ "verified_phone_label_enabled": False,
485
+ }
486
+ query = {"variables": variables, "features": features}
487
+ response, data = await self.request("GET", url, params=query)
488
+
489
+ users = {}
490
+ for user_data in data["data"]["users"]:
491
+ user_data = user_data["result"]
492
+ user = User.from_raw_data(user_data)
493
+ users[user.id] = user
494
+ if user.id == self.account.id:
495
+ users[self.account.id] = self.account
496
+ return users
497
+
498
+ async def request_user_by_id(self, user_id: int | str) -> User | Account | None:
499
+ """
500
+ :param user_id: ID пользователя
501
+ :return: Пользователь, если существует, иначе None. Или собственный аккаунт, если совпадает ID.
502
+ """
503
+ if not self.account.id:
504
+ await self.update_account_info()
505
+
506
+ users = await self._request_users_by_ids((user_id,))
507
+ user = users[user_id]
508
+ return user
509
+
510
+ async def request_users_by_ids(
511
+ self, user_ids: Iterable[str | int]
512
+ ) -> dict[int : User | Account]:
513
+ """
514
+ :param user_ids: ID пользователей
515
+ :return: Пользователи, если существует, иначе None. Или собственный аккаунт, если совпадает ID.
516
+ """
517
+ return await self._request_users_by_ids(user_ids)
518
+
519
+ async def update_account_info(self):
520
+ if not self.account.username:
521
+ await self._update_account_username()
522
+
523
+ await self.request_user_by_username(self.account.username)
524
+
482
525
  async def upload_image(
483
526
  self,
484
527
  image: bytes,
@@ -494,9 +537,7 @@ class Client(BaseHTTPClient):
494
537
  :return: Media
495
538
  """
496
539
  url = "https://upload.twitter.com/1.1/media/upload.json"
497
-
498
540
  payload = {"media_data": base64.b64encode(image)}
499
-
500
541
  for attempt in range(attempts):
501
542
  try:
502
543
  response, data = await self.request(
@@ -606,6 +647,8 @@ class Client(BaseHTTPClient):
606
647
  """
607
648
  Repost (retweet)
608
649
 
650
+ Иногда может вернуть ошибку 404 (Not Found), если плохой прокси или по другим неизвестным причинам
651
+
609
652
  :return: Tweet
610
653
  """
611
654
  return await self._repost_or_search_duplicate(
@@ -613,9 +656,18 @@ class Client(BaseHTTPClient):
613
656
  )
614
657
 
615
658
  async def like(self, tweet_id: int) -> bool:
616
- response_json = await self._interact_with_tweet("FavoriteTweet", tweet_id)
617
- is_liked = response_json["data"]["favorite_tweet"] == "Done"
618
- return is_liked
659
+ """
660
+ :return: Liked or not
661
+ """
662
+ try:
663
+ response_json = await self._interact_with_tweet("FavoriteTweet", tweet_id)
664
+ except HTTPException as exc:
665
+ if 139 in exc.api_codes:
666
+ # Already liked
667
+ return True
668
+ else:
669
+ raise
670
+ return response_json["data"]["favorite_tweet"] == "Done"
619
671
 
620
672
  async def unlike(self, tweet_id: int) -> dict:
621
673
  response_json = await self._interact_with_tweet("UnfavoriteTweet", tweet_id)
@@ -662,47 +714,50 @@ class Client(BaseHTTPClient):
662
714
  attachment_url: str = None,
663
715
  ) -> Tweet:
664
716
  url, query_id = self._action_to_url("CreateTweet")
665
- payload = {
666
- "variables": {
667
- "tweet_text": text if text is not None else "",
668
- "dark_request": False,
669
- "media": {"media_entities": [], "possibly_sensitive": False},
670
- "semantic_annotation_ids": [],
671
- },
672
- "features": {
673
- "tweetypie_unmention_optimization_enabled": True,
674
- "responsive_web_edit_tweet_api_enabled": True,
675
- "graphql_is_translatable_rweb_tweet_is_translatable_enabled": True,
676
- "view_counts_everywhere_api_enabled": True,
677
- "longform_notetweets_consumption_enabled": True,
678
- "tweet_awards_web_tipping_enabled": False,
679
- "longform_notetweets_rich_text_read_enabled": True,
680
- "longform_notetweets_inline_media_enabled": True,
681
- "responsive_web_graphql_exclude_directive_enabled": True,
682
- "verified_phone_label_enabled": False,
683
- "freedom_of_speech_not_reach_fetch_enabled": True,
684
- "standardized_nudges_misinfo": True,
685
- "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": False,
686
- "responsive_web_graphql_skip_user_profile_image_extensions_enabled": False,
687
- "responsive_web_graphql_timeline_navigation_enabled": True,
688
- "responsive_web_enhance_cards_enabled": False,
689
- "responsive_web_twitter_article_tweet_consumption_enabled": False,
690
- "responsive_web_media_download_video_enabled": False,
691
- },
692
- "queryId": query_id,
717
+ variables = {
718
+ "tweet_text": text if text is not None else "",
719
+ "dark_request": False,
720
+ "media": {"media_entities": [], "possibly_sensitive": False},
721
+ "semantic_annotation_ids": [],
693
722
  }
694
723
  if attachment_url:
695
- payload["variables"]["attachment_url"] = attachment_url
724
+ variables["attachment_url"] = attachment_url
696
725
  if tweet_id_to_reply:
697
- payload["variables"]["reply"] = {
726
+ variables["reply"] = {
698
727
  "in_reply_to_tweet_id": str(tweet_id_to_reply),
699
728
  "exclude_reply_user_ids": [],
700
729
  }
701
730
  if media_id:
702
- payload["variables"]["media"]["media_entities"].append(
731
+ variables["media"]["media_entities"].append(
703
732
  {"media_id": str(media_id), "tagged_users": []}
704
733
  )
705
-
734
+ features = {
735
+ "communities_web_enable_tweet_community_results_fetch": True,
736
+ "c9s_tweet_anatomy_moderator_badge_enabled": True,
737
+ "tweetypie_unmention_optimization_enabled": True,
738
+ "responsive_web_edit_tweet_api_enabled": True,
739
+ "graphql_is_translatable_rweb_tweet_is_translatable_enabled": True,
740
+ "view_counts_everywhere_api_enabled": True,
741
+ "longform_notetweets_consumption_enabled": True,
742
+ "responsive_web_twitter_article_tweet_consumption_enabled": True,
743
+ "tweet_awards_web_tipping_enabled": False,
744
+ "longform_notetweets_rich_text_read_enabled": True,
745
+ "longform_notetweets_inline_media_enabled": True,
746
+ "rweb_video_timestamps_enabled": True,
747
+ "responsive_web_graphql_exclude_directive_enabled": True,
748
+ "verified_phone_label_enabled": False,
749
+ "freedom_of_speech_not_reach_fetch_enabled": True,
750
+ "standardized_nudges_misinfo": True,
751
+ "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": True,
752
+ "responsive_web_graphql_skip_user_profile_image_extensions_enabled": False,
753
+ "responsive_web_graphql_timeline_navigation_enabled": True,
754
+ "responsive_web_enhance_cards_enabled": False,
755
+ }
756
+ payload = {
757
+ "variables": variables,
758
+ "features": features,
759
+ "queryId": query_id,
760
+ }
706
761
  response, response_json = await self.request("POST", url, json=payload)
707
762
  tweet = Tweet.from_raw_data(
708
763
  response_json["data"]["create_tweet"]["tweet_results"]["result"]
@@ -754,6 +809,11 @@ class Client(BaseHTTPClient):
754
809
  media_id: int | str = None,
755
810
  search_duplicate: bool = True,
756
811
  ) -> Tweet:
812
+ """
813
+ Иногда может вернуть ошибку 404 (Not Found), если плохой прокси или по другим неизвестным причинам
814
+
815
+ :return: Tweet
816
+ """
757
817
  return await self._tweet_or_search_duplicate(
758
818
  text,
759
819
  media_id=media_id,
@@ -768,6 +828,11 @@ class Client(BaseHTTPClient):
768
828
  media_id: int | str = None,
769
829
  search_duplicate: bool = True,
770
830
  ) -> Tweet:
831
+ """
832
+ Иногда может вернуть ошибку 404 (Not Found), если плохой прокси или по другим неизвестным причинам
833
+
834
+ :return: Tweet
835
+ """
771
836
  return await self._tweet_or_search_duplicate(
772
837
  text,
773
838
  media_id=media_id,
@@ -783,6 +848,11 @@ class Client(BaseHTTPClient):
783
848
  media_id: int | str = None,
784
849
  search_duplicate: bool = True,
785
850
  ) -> Tweet:
851
+ """
852
+ Иногда может вернуть ошибку 404 (Not Found), если плохой прокси или по другим неизвестным причинам
853
+
854
+ :return: Tweet
855
+ """
786
856
  return await self._tweet_or_search_duplicate(
787
857
  text,
788
858
  media_id=media_id,
@@ -807,8 +877,12 @@ class Client(BaseHTTPClient):
807
877
  response, response_json = await self.request("POST", url, params=params)
808
878
  return response_json
809
879
 
810
- async def _request_users(
811
- self, action: str, user_id: int | str, count: int
880
+ async def _request_users_by_action(
881
+ self,
882
+ action: str,
883
+ user_id: int | str,
884
+ count: int,
885
+ cursor: str = None,
812
886
  ) -> list[User]:
813
887
  url, query_id = self._action_to_url(action)
814
888
  variables = {
@@ -816,6 +890,8 @@ class Client(BaseHTTPClient):
816
890
  "count": count,
817
891
  "includePromotedContent": False,
818
892
  }
893
+ if cursor:
894
+ variables["cursor"] = cursor
819
895
  features = {
820
896
  "rweb_lists_timeline_redesign_enabled": True,
821
897
  "responsive_web_graphql_exclude_directive_enabled": True,
@@ -858,53 +934,46 @@ class Client(BaseHTTPClient):
858
934
  return users
859
935
 
860
936
  async def request_followers(
861
- self, user_id: int | str = None, count: int = 10
937
+ self,
938
+ user_id: int | str = None,
939
+ count: int = 20,
940
+ cursor: str = None,
862
941
  ) -> list[User]:
863
942
  """
864
943
  :param user_id: Текущий пользователь, если не передан ID иного пользователя.
865
944
  :param count: Количество подписчиков.
866
945
  """
867
946
  if user_id:
868
- return await self._request_users("Followers", user_id, count)
947
+ return await self._request_users_by_action(
948
+ "Followers", user_id, count, cursor
949
+ )
869
950
  else:
870
951
  if not self.account.id:
871
- await self.request_user()
872
- return await self._request_users("Followers", self.account.id, count)
952
+ await self.update_account_info()
953
+ return await self._request_users_by_action(
954
+ "Followers", self.account.id, count, cursor
955
+ )
873
956
 
874
957
  async def request_followings(
875
- self, user_id: int | str = None, count: int = 10
958
+ self,
959
+ user_id: int | str = None,
960
+ count: int = 20,
961
+ cursor: str = None,
876
962
  ) -> list[User]:
877
963
  """
878
964
  :param user_id: Текущий пользователь, если не передан ID иного пользователя.
879
965
  :param count: Количество подписчиков.
880
966
  """
881
967
  if user_id:
882
- return await self._request_users("Following", user_id, count)
968
+ return await self._request_users_by_action(
969
+ "Following", user_id, count, cursor
970
+ )
883
971
  else:
884
972
  if not self.account.id:
885
- await self.request_user()
886
- return await self._request_users("Following", self.account.id, count)
887
-
888
- async def request_users(
889
- self, user_ids: Iterable[str | int]
890
- ) -> dict[int : User | Account]:
891
- url, query_id = self._action_to_url("UsersByRestIds")
892
- variables = {"userIds": list({str(user_id) for user_id in user_ids})}
893
- features = {
894
- "responsive_web_graphql_exclude_directive_enabled": True,
895
- "responsive_web_graphql_skip_user_profile_image_extensions_enabled": False,
896
- "responsive_web_graphql_timeline_navigation_enabled": True,
897
- "verified_phone_label_enabled": False,
898
- }
899
- query = {"variables": variables, "features": features}
900
- response, data = await self.request("GET", url, params=query)
901
-
902
- users = {}
903
- for user_data in data["data"]["users"]:
904
- user_data = user_data["result"]
905
- user = User.from_raw_data(user_data)
906
- users[user.id] = user
907
- return users
973
+ await self.update_account_info()
974
+ return await self._request_users_by_action(
975
+ "Following", self.account.id, count, cursor
976
+ )
908
977
 
909
978
  async def _request_tweet(self, tweet_id: int | str) -> Tweet:
910
979
  url, query_id = self._action_to_url("TweetDetail")
@@ -947,7 +1016,9 @@ class Client(BaseHTTPClient):
947
1016
  tweet_data = tweets_data_from_instructions(instructions)[0]
948
1017
  return Tweet.from_raw_data(tweet_data)
949
1018
 
950
- async def _request_tweets(self, user_id: int | str, count: int = 20) -> list[Tweet]:
1019
+ async def _request_tweets(
1020
+ self, user_id: int | str, count: int = 20, cursor: str = None
1021
+ ) -> list[Tweet]:
951
1022
  url, query_id = self._action_to_url("UserTweets")
952
1023
  variables = {
953
1024
  "userId": str(user_id),
@@ -957,6 +1028,8 @@ class Client(BaseHTTPClient):
957
1028
  "withVoice": True,
958
1029
  "withV2Timeline": True,
959
1030
  }
1031
+ if cursor:
1032
+ variables["cursor"] = cursor
960
1033
  features = {
961
1034
  "responsive_web_graphql_exclude_directive_enabled": True,
962
1035
  "verified_phone_label_enabled": False,
@@ -993,16 +1066,14 @@ class Client(BaseHTTPClient):
993
1066
  return await self._request_tweet(tweet_id)
994
1067
 
995
1068
  async def request_tweets(
996
- self,
997
- user_id: int | str = None,
998
- count: int = 20,
1069
+ self, user_id: int | str = None, count: int = 20, cursor: str = None
999
1070
  ) -> list[Tweet]:
1000
1071
  if not user_id:
1001
1072
  if not self.account.id:
1002
- await self.request_user()
1073
+ await self.update_account_info()
1003
1074
  user_id = self.account.id
1004
1075
 
1005
- return await self._request_tweets(user_id, count)
1076
+ return await self._request_tweets(user_id, count, cursor)
1006
1077
 
1007
1078
  async def _update_profile_image(
1008
1079
  self, type: Literal["banner", "image"], media_id: str | int
@@ -1068,9 +1139,6 @@ class Client(BaseHTTPClient):
1068
1139
  }
1069
1140
  response, data = await self.request("POST", url, data=payload)
1070
1141
  changed = data["status"] == "ok"
1071
- # TODO Делать это автоматически в методе request
1072
- auth_token = response.cookies.get("auth_token", domain=".twitter.com")
1073
- self.account.auth_token = auth_token
1074
1142
  self.account.password = password
1075
1143
  return changed
1076
1144
 
@@ -1088,7 +1156,6 @@ class Client(BaseHTTPClient):
1088
1156
  raise ValueError("Specify at least one param")
1089
1157
 
1090
1158
  url = "https://twitter.com/i/api/1.1/account/update_profile.json"
1091
- # headers = {"content-type": "application/x-www-form-urlencoded"}
1092
1159
  # Создаем словарь data, включая в него только те ключи, для которых значения не равны None
1093
1160
  payload = {
1094
1161
  k: v
@@ -1100,7 +1167,6 @@ class Client(BaseHTTPClient):
1100
1167
  ]
1101
1168
  if v is not None
1102
1169
  }
1103
- # response, response_json = await self.request("POST", url, headers=headers, data=payload)
1104
1170
  response, data = await self.request("POST", url, data=payload)
1105
1171
  # Проверяем, что все переданные параметры соответствуют полученным
1106
1172
  updated = all(
@@ -1110,7 +1176,7 @@ class Client(BaseHTTPClient):
1110
1176
  updated &= URL(website) == URL(
1111
1177
  data["entities"]["url"]["urls"][0]["expanded_url"]
1112
1178
  )
1113
- await self.request_user()
1179
+ await self.update_account_info()
1114
1180
  return updated
1115
1181
 
1116
1182
  async def establish_status(self):
@@ -1129,17 +1195,14 @@ class Client(BaseHTTPClient):
1129
1195
  year_visibility: Literal["self"] = "self",
1130
1196
  ) -> bool:
1131
1197
  url = "https://twitter.com/i/api/1.1/account/update_profile.json"
1132
- headers = {"content-type": "application/x-www-form-urlencoded"}
1133
- data = {
1198
+ payload = {
1134
1199
  "birthdate_day": day,
1135
1200
  "birthdate_month": month,
1136
1201
  "birthdate_year": year,
1137
1202
  "birthdate_visibility": visibility,
1138
1203
  "birthdate_year_visibility": year_visibility,
1139
1204
  }
1140
- response, response_json = await self.request(
1141
- "POST", url, headers=headers, data=data
1142
- )
1205
+ response, response_json = await self.request("POST", url, json=payload)
1143
1206
  birthdate_data = response_json["extended_profile"]["birthdate"]
1144
1207
  updated = all(
1145
1208
  (
@@ -1249,6 +1312,8 @@ class Client(BaseHTTPClient):
1249
1312
  payload["verification_string"] = verification_string
1250
1313
  payload["language_code"] = "en"
1251
1314
 
1315
+ # TODO ui_metrics
1316
+
1252
1317
  return await self.request("POST", self._CAPTCHA_URL, data=payload, bearer=False)
1253
1318
 
1254
1319
  async def unlock(self):
@@ -1262,9 +1327,23 @@ class Client(BaseHTTPClient):
1262
1327
  needs_unlock,
1263
1328
  start_button,
1264
1329
  finish_button,
1330
+ delete_button,
1265
1331
  ) = parse_unlock_html(html)
1266
1332
  attempt = 1
1267
1333
 
1334
+ if delete_button:
1335
+ response, html = await self._confirm_unlock(
1336
+ authenticity_token, assignment_token
1337
+ )
1338
+ (
1339
+ authenticity_token,
1340
+ assignment_token,
1341
+ needs_unlock,
1342
+ start_button,
1343
+ finish_button,
1344
+ delete_button,
1345
+ ) = parse_unlock_html(html)
1346
+
1268
1347
  if start_button or finish_button:
1269
1348
  response, html = await self._confirm_unlock(
1270
1349
  authenticity_token, assignment_token
@@ -1275,6 +1354,7 @@ class Client(BaseHTTPClient):
1275
1354
  needs_unlock,
1276
1355
  start_button,
1277
1356
  finish_button,
1357
+ delete_button,
1278
1358
  ) = parse_unlock_html(html)
1279
1359
 
1280
1360
  funcaptcha = {
@@ -1321,6 +1401,7 @@ class Client(BaseHTTPClient):
1321
1401
  needs_unlock,
1322
1402
  start_button,
1323
1403
  finish_button,
1404
+ delete_button,
1324
1405
  ) = parse_unlock_html(html)
1325
1406
 
1326
1407
  if finish_button:
@@ -1333,6 +1414,7 @@ class Client(BaseHTTPClient):
1333
1414
  needs_unlock,
1334
1415
  start_button,
1335
1416
  finish_button,
1417
+ delete_button,
1336
1418
  ) = parse_unlock_html(html)
1337
1419
 
1338
1420
  attempt += 1
@@ -1565,7 +1647,7 @@ class Client(BaseHTTPClient):
1565
1647
 
1566
1648
  async def totp_is_enabled(self):
1567
1649
  if not self.account.id:
1568
- await self.request_user()
1650
+ await self.update_account_info()
1569
1651
 
1570
1652
  url = f"https://twitter.com/i/api/1.1/strato/column/User/{self.account.id}/account-security/twoFactorAuthSettings2"
1571
1653
  response, data = await self.request("GET", url)
twitter/utils/html.py CHANGED
@@ -21,9 +21,11 @@ def parse_oauth_html(html: str) -> tuple[str | None, str | None, str | None]:
21
21
  return authenticity_token, redirect_url, redirect_after_login_url
22
22
 
23
23
 
24
- def parse_unlock_html(html: str) -> tuple[str | None, str | None, bool, bool, bool]:
24
+ def parse_unlock_html(
25
+ html: str,
26
+ ) -> tuple[str | None, str | None, bool, bool, bool, bool]:
25
27
  """
26
- :return: authenticity_token, assignment_token, needs_unlock, start_button, finish_button
28
+ :return: authenticity_token, assignment_token, needs_unlock, start_button, finish_button, delete_button
27
29
  """
28
30
  soup = BeautifulSoup(html, "lxml")
29
31
  authenticity_token_element = soup.find("input", {"name": "authenticity_token"})
@@ -38,10 +40,12 @@ def parse_unlock_html(html: str) -> tuple[str | None, str | None, bool, bool, bo
38
40
  needs_unlock = bool(verification_string)
39
41
  start_button = bool(soup.find("input", value="Start"))
40
42
  finish_button = bool(soup.find("input", value="Continue to X"))
43
+ delete_button = bool(soup.find("input", value="Delete"))
41
44
  return (
42
45
  authenticity_token,
43
46
  assignment_token,
44
47
  needs_unlock,
45
48
  start_button,
46
49
  finish_button,
50
+ delete_button,
47
51
  )
@@ -1,225 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: tweepy-self
3
- Version: 1.10.0b1
4
- Summary: Twitter (selfbot) for Python!
5
- Home-page: https://github.com/alenkimov/tweepy-self
6
- Author: Alen
7
- Author-email: alen.kimov@gmail.com
8
- Requires-Python: >=3.11,<4.0
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: Programming Language :: Python :: 3.11
11
- Classifier: Programming Language :: Python :: 3.12
12
- Requires-Dist: aiohttp (>=3.9,<4.0)
13
- Requires-Dist: beautifulsoup4 (>=4,<5)
14
- Requires-Dist: better-proxy (>=1.1,<2.0)
15
- Requires-Dist: curl_cffi (==0.6.2)
16
- Requires-Dist: loguru (>=0.7,<0.8)
17
- Requires-Dist: lxml (>=5,<6)
18
- Requires-Dist: pydantic (>=1)
19
- Requires-Dist: pyotp (>=2,<3)
20
- Requires-Dist: requests (>=2,<3)
21
- Requires-Dist: tenacity (>=8,<9)
22
- Requires-Dist: yarl (>=1,<2)
23
- Project-URL: Repository, https://github.com/alenkimov/tweepy-self
24
- Project-URL: Source, https://github.com/alenkimov/tweepy-self
25
- Description-Content-Type: text/markdown
26
-
27
- # Tweepy-self
28
- [![Telegram channel](https://img.shields.io/endpoint?url=https://runkit.io/damiankrawczyk/telegram-badge/branches/master?url=https://t.me/cum_insider)](https://t.me/cum_insider)
29
- [![PyPI version info](https://img.shields.io/pypi/v/tweepy-self.svg)](https://pypi.python.org/pypi/tweepy-self)
30
- [![PyPI supported Python versions](https://img.shields.io/pypi/pyversions/tweepy-self.svg)](https://pypi.python.org/pypi/tweepy-self)
31
- [![PyPI downloads per month](https://img.shields.io/pypi/dm/tweepy-self.svg)](https://pypi.python.org/pypi/tweepy-self)
32
-
33
- A modern, easy to use, feature-rich, and async ready API wrapper for Twitter's user API written in Python.
34
-
35
- - Docs (soon)
36
-
37
- More libraries of the family:
38
- - [better-proxy](https://github.com/alenkimov/better_proxy)
39
- - [better-web3](https://github.com/alenkimov/better_web3)
40
-
41
- Отдельное спасибо [Кузнице Ботов](https://t.me/bots_forge) за код для авторизации и разморозки! Подписывайтесь на их Telegram :)
42
-
43
- ## Key Features
44
- - Modern Pythonic API using async and await.
45
- - Prevents user account automation detection.
46
-
47
- ## Installing
48
- ```bash
49
- pip install tweepy-self
50
- ```
51
-
52
- ## Example
53
- ```python
54
- import asyncio
55
- import twitter
56
-
57
- account = twitter.Account(auth_token="auth_token")
58
-
59
- async def main():
60
- async with twitter.Client(account) as twitter_client:
61
- await twitter_client.tweet("Hello, tweepy-self! <3")
62
-
63
- asyncio.run(main())
64
- ```
65
-
66
- ## More
67
- Automating user accounts is against the Twitter ToS. This library is a proof of concept and I cannot recommend using it. Do so at your own risk
68
-
69
- ## Документация (устаревшая)
70
- `Код ушел немного дальше, чем эта документация.`
71
-
72
- Библиотека позволяет работать с неофициальным API Twitter, а именно:
73
- - Логин
74
- - Анлок
75
- - Привязывать сервисы (приложения).
76
- - Устанавливать статус аккаунта (бан, лок).
77
- - Загружать изображения на сервер и изменять баннер и аватарку.
78
- - Изменять данные о пользователе: имя, описание профиля и другое.
79
- - Изменять имя пользователя и пароль.
80
- - Запрашивать информацию о подписчиках.
81
- - Запрашивать некоторую информацию о пользователе (количество подписчиков и другое).
82
- - Голосовать.
83
- - Подписываться и отписываться.
84
- - Лайкать и дизлайкать.
85
- - Твиттить, ретвиттить с изображением и без.
86
- - Закреплять твиты.
87
- - Запрашивать твиты пользователей.
88
- - Удалять твиты.
89
- - И другое.
90
-
91
- #### Статус аккаунта
92
- После любого взаимодействия с Twitter устанавливается статус аккаунта:
93
- - `BAD_TOKEN` - Неверный токен.
94
- - `UNKNOWN` - Статус аккаунта не установлен.
95
- - `SUSPENDED` - Действие учетной записи приостановлено (бан).
96
- - `LOCKED` - Учетная запись заморожена (лок) (требуется прохождение капчи).
97
- - `GOOD` - Аккаунт в порядке.
98
-
99
- Не каждое взаимодействие с Twitter достоверно определяет статус аккаунта.
100
- Например, простой запрос данных об аккаунте честно вернет данные, даже если ваш аккаунт заморожен.
101
-
102
- Для достоверной установки статуса аккаунта используйте метод `establish_status()`
103
-
104
- ### Примеры работы
105
- Запрос информации о пользователе:
106
-
107
- ```python
108
- # Запрос информации о текущем пользователе:
109
- me = await twitter_client.request_user()
110
- print(f"[{account.short_auth_token}] {me}")
111
- print(f"Аккаунт создан: {me.created_at}")
112
- print(f"Following (подписан ты): {me.followings_count}")
113
- print(f"Followers (подписаны на тебя): {me.followers_count}")
114
- print(f"Прочая информация: {me.raw_data}")
115
-
116
- # Запрос информации об ином пользователе:
117
- elonmusk = await twitter.request_user("@elonmusk")
118
- print(elonmusk)
119
- ```
120
-
121
- Смена имени пользователя и пароля:
122
-
123
- ```python
124
- account = twitter.Account("auth_token", password="password")
125
- ...
126
- await twitter_client.change_username("new_username")
127
- await twitter_client.request_user()
128
- print(f"New username: {account.username}")
129
-
130
- await twitter_client.change_password("new_password")
131
- print(f"New password: {account.password}")
132
- print(f"New auth_token: {account.auth_token}")
133
- ```
134
-
135
- Смена данных профиля:
136
- ```python
137
- await twitter_client.update_birthdate(day=1, month=12, year=2000)
138
- await twitter_client.update_profile( # Locks account!
139
- name="New Name",
140
- description="New description",
141
- location="New York",
142
- website="https://github.com/alenkimov/better_automation",
143
- )
144
- ```
145
-
146
- Загрузка изображений и смена аватара и баннера:
147
- ```python
148
- image = open(f"image.png", "rb").read()
149
- media_id = await twitter_client.upload_image(image)
150
- avatar_image_url = await twitter_client.update_profile_avatar(media_id)
151
- banner_image_url = await twitter_client.update_profile_banner(media_id)
152
- ```
153
-
154
- Привязка сервиса (приложения):
155
-
156
- ```python
157
- # Изучите запросы сервиса и найдите подобные данные для авторизации (привязки):
158
- bind_data = {
159
- 'response_type': 'code',
160
- 'client_id': 'TjFVQm52ZDFGWEtNT0tKaktaSWU6MTpjaQ',
161
- 'redirect_uri': 'https://waitlist.lens.xyz/tw/',
162
- 'scope': 'users.read tweet.read offline.access',
163
- 'state': 'state', # Может быть как статичным, так и динамическим.
164
- 'code_challenge': 'challenge',
165
- 'code_challenge_method': 'plain'
166
- }
167
-
168
- bind_code = await twitter_client.oauth_2(**bind_data)
169
- # Передайте код авторизации (привязки) сервису.
170
- # Сервис также может потребовать state, если он динамический.
171
- ```
172
-
173
- Отправка сообщения:
174
-
175
- ```python
176
- bro = await twitter_client.request_user("@username")
177
- await twitter_client.send_message(bro.id, "I love you!")
178
- ```
179
-
180
- Запрос входящих сообщений:
181
- ```python
182
- messages = await twitter_client.request_messages()
183
- for message in messages:
184
- message_data = message["message_data"]
185
- recipient_id = message_data["recipient_id"]
186
- sender_id = message_data["sender_id"]
187
- text = message_data["text"]
188
- print(f"[id {sender_id}] -> [id {recipient_id}]: {text}")
189
- ```
190
-
191
- Другие методы:
192
- ```python
193
- # Выражение любви через твит
194
- tweet_id = await twitter_client.tweet("I love YOU! !!!!1!1")
195
- print(f"Любовь выражена! Tweet id: {tweet_id}")
196
-
197
- print(f"Tweet is pined: {await twitter_client.pin_tweet(tweet_id)}")
198
-
199
- # Лайк
200
- print(f"Tweet {tweet_id} is liked: {await twitter_client.like(tweet_id)}")
201
-
202
- # Репост (ретвит)
203
- print(f"Tweet {tweet_id} is retweeted. Tweet id: {await twitter_client.repost(tweet_id)}")
204
-
205
- # Коммент (реплай)
206
- print(f"Tweet {tweet_id} is replied. Reply id: {await twitter_client.reply(tweet_id, 'tem razão')}")
207
-
208
- # Подписываемся на Илона Маска
209
- print(f"@{elonmusk.username} is followed: {await twitter_client.follow(elonmusk.id)}")
210
-
211
- # Отписываемся от Илона Маска
212
- print(f"@{elonmusk.username} is unfollowed: {await twitter_client.unfollow(elonmusk.id)}")
213
-
214
- tweet_url = 'https://twitter.com/CreamIce_Cone/status/1691735090529976489'
215
- # Цитата (Quote tweet)
216
- quote_tweet_id = await twitter_client.quote(tweet_url, 'oh....')
217
- print(f"Quoted! Tweet id: {quote_tweet_id}")
218
-
219
- # Запрашиваем первых трех подписчиков
220
- # (Параметр count по каким-то причинам работает некорректно)
221
- followers = await twitter_client.request_followers(count=20)
222
- print("Твои подписчики:")
223
- for follower in followers:
224
- print(follower)
225
- ```