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.
- 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
|