tweepy-self 1.10.0b1__py3-none-any.whl → 1.10.0b4__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- tweepy_self-1.10.0b4.dist-info/METADATA +303 -0
- {tweepy_self-1.10.0b1.dist-info → tweepy_self-1.10.0b4.dist-info}/RECORD +5 -5
- twitter/client.py +197 -115
- twitter/utils/html.py +6 -2
- tweepy_self-1.10.0b1.dist-info/METADATA +0 -225
- {tweepy_self-1.10.0b1.dist-info → tweepy_self-1.10.0b4.dist-info}/WHEEL +0 -0
@@ -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
|
+
[](https://t.me/cum_insider)
|
29
|
+
[](https://pypi.python.org/pypi/tweepy-self)
|
30
|
+
[](https://pypi.python.org/pypi/tweepy-self)
|
31
|
+
[](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=
|
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=
|
19
|
+
twitter/utils/html.py,sha256=nrOJw0vUKfBaHgFaQSQIdXfvfZ8mdu84MU_s46kJTJ4,2087
|
20
20
|
twitter/utils/other.py,sha256=9RIYF2AMdmNKIwClG3jBP7zlvxZPEgYfuHaIiOhURzM,1061
|
21
|
-
tweepy_self-1.10.
|
22
|
-
tweepy_self-1.10.
|
23
|
-
tweepy_self-1.10.
|
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": "
|
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
|
-
|
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.
|
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.
|
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.
|
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.
|
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.
|
299
|
-
await self.
|
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
|
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
|
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
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
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
|
-
|
467
|
+
user = await self._request_user_by_username(username)
|
475
468
|
|
476
|
-
if
|
469
|
+
if user and user.id == self.account.id:
|
477
470
|
self.account.update(**user.model_dump())
|
478
|
-
|
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
|
-
|
617
|
-
|
618
|
-
|
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
|
-
|
666
|
-
"
|
667
|
-
|
668
|
-
|
669
|
-
|
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
|
-
|
724
|
+
variables["attachment_url"] = attachment_url
|
696
725
|
if tweet_id_to_reply:
|
697
|
-
|
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
|
-
|
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
|
811
|
-
self,
|
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,
|
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.
|
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.
|
872
|
-
return await self.
|
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,
|
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.
|
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.
|
886
|
-
return await self.
|
887
|
-
|
888
|
-
|
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(
|
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.
|
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.
|
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
|
-
|
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.
|
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(
|
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
|
-
[](https://t.me/cum_insider)
|
29
|
-
[](https://pypi.python.org/pypi/tweepy-self)
|
30
|
-
[](https://pypi.python.org/pypi/tweepy-self)
|
31
|
-
[](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
|
-
```
|
File without changes
|