tweepy-self 1.6.3__py3-none-any.whl → 1.10.0b1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- {tweepy_self-1.6.3.dist-info → tweepy_self-1.10.0b1.dist-info}/METADATA +16 -9
- tweepy_self-1.10.0b1.dist-info/RECORD +23 -0
- twitter/__init__.py +15 -5
- twitter/_capsolver/__init__.py +0 -0
- twitter/_capsolver/core/__init__.py +0 -0
- twitter/_capsolver/core/base.py +227 -0
- twitter/_capsolver/core/config.py +36 -0
- twitter/_capsolver/core/enum.py +66 -0
- twitter/_capsolver/core/serializer.py +85 -0
- twitter/_capsolver/fun_captcha.py +260 -0
- twitter/account.py +17 -13
- twitter/base/__init__.py +2 -2
- twitter/base/client.py +4 -4
- twitter/client.py +388 -222
- twitter/errors.py +14 -7
- twitter/models.py +126 -50
- twitter/utils/__init__.py +2 -0
- twitter/utils/other.py +13 -0
- tweepy_self-1.6.3.dist-info/RECORD +0 -16
- {tweepy_self-1.6.3.dist-info → tweepy_self-1.10.0b1.dist-info}/WHEEL +0 -0
twitter/client.py
CHANGED
@@ -1,13 +1,15 @@
|
|
1
|
-
from typing import Any, Literal
|
1
|
+
from typing import Any, Literal, Iterable
|
2
2
|
from time import time
|
3
3
|
import asyncio
|
4
4
|
import base64
|
5
|
+
import json
|
5
6
|
import re
|
6
7
|
|
8
|
+
from loguru import logger
|
7
9
|
from curl_cffi import requests
|
8
10
|
from yarl import URL
|
9
11
|
|
10
|
-
from
|
12
|
+
from ._capsolver.fun_captcha import FunCaptcha, FunCaptchaTypeEnm
|
11
13
|
|
12
14
|
from .errors import (
|
13
15
|
TwitterException,
|
@@ -25,20 +27,24 @@ from .errors import (
|
|
25
27
|
ConsentLocked,
|
26
28
|
Suspended,
|
27
29
|
)
|
28
|
-
from .utils import to_json
|
29
|
-
from .base import
|
30
|
+
from .utils import to_json
|
31
|
+
from .base import BaseHTTPClient
|
30
32
|
from .account import Account, AccountStatus
|
31
|
-
from .models import
|
32
|
-
from .utils import
|
33
|
+
from .models import User, Tweet, Media
|
34
|
+
from .utils import (
|
35
|
+
remove_at_sign,
|
36
|
+
parse_oauth_html,
|
37
|
+
parse_unlock_html,
|
38
|
+
tweets_data_from_instructions,
|
39
|
+
)
|
33
40
|
|
34
41
|
|
35
|
-
class Client(
|
36
|
-
_BEARER_TOKEN = "
|
42
|
+
class Client(BaseHTTPClient):
|
43
|
+
_BEARER_TOKEN = "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"
|
37
44
|
_DEFAULT_HEADERS = {
|
38
45
|
"authority": "twitter.com",
|
39
46
|
"origin": "https://twitter.com",
|
40
47
|
"x-twitter-active-user": "yes",
|
41
|
-
# 'x-twitter-auth-type': 'OAuth2Session',
|
42
48
|
"x-twitter-client-language": "en",
|
43
49
|
}
|
44
50
|
_GRAPHQL_URL = "https://twitter.com/i/api/graphql"
|
@@ -56,6 +62,7 @@ class Client(BaseClient):
|
|
56
62
|
"Following": "t-BPOrMIduGUJWO_LxcvNQ",
|
57
63
|
"Followers": "3yX7xr2hKjcZYnXt6cU6lQ",
|
58
64
|
"UserByScreenName": "G3KGOASz96M-Qu0nwmGXNg",
|
65
|
+
"UsersByRestIds": "itEhGywpgX9b3GJCzOtSrA",
|
59
66
|
"Viewer": "W62NnYgkgziw9bwyoVht0g",
|
60
67
|
}
|
61
68
|
_CAPTCHA_URL = "https://twitter.com/account/access"
|
@@ -76,7 +83,9 @@ class Client(BaseClient):
|
|
76
83
|
*,
|
77
84
|
wait_on_rate_limit: bool = True,
|
78
85
|
capsolver_api_key: str = None,
|
79
|
-
max_unlock_attempts: int =
|
86
|
+
max_unlock_attempts: int = 5,
|
87
|
+
auto_relogin: bool = True,
|
88
|
+
request_self_on_startup: bool = True,
|
80
89
|
**session_kwargs,
|
81
90
|
):
|
82
91
|
super().__init__(**session_kwargs)
|
@@ -84,20 +93,29 @@ class Client(BaseClient):
|
|
84
93
|
self.wait_on_rate_limit = wait_on_rate_limit
|
85
94
|
self.capsolver_api_key = capsolver_api_key
|
86
95
|
self.max_unlock_attempts = max_unlock_attempts
|
96
|
+
self.auto_relogin = auto_relogin
|
97
|
+
self.request_self_on_startup = request_self_on_startup
|
87
98
|
|
88
|
-
async def
|
99
|
+
async def __aenter__(self):
|
100
|
+
await self.on_startup()
|
101
|
+
return await super().__aenter__()
|
102
|
+
|
103
|
+
async def _request(
|
89
104
|
self,
|
90
105
|
method,
|
91
106
|
url,
|
107
|
+
*,
|
92
108
|
auth: bool = True,
|
93
109
|
bearer: bool = True,
|
110
|
+
wait_on_rate_limit: bool = None,
|
94
111
|
**kwargs,
|
95
112
|
) -> tuple[requests.Response, Any]:
|
96
113
|
cookies = kwargs["cookies"] = kwargs.get("cookies") or {}
|
97
114
|
headers = kwargs["headers"] = kwargs.get("headers") or {}
|
98
115
|
|
99
116
|
if bearer:
|
100
|
-
headers["authorization"] = self._BEARER_TOKEN
|
117
|
+
headers["authorization"] = f"Bearer {self._BEARER_TOKEN}"
|
118
|
+
# headers["x-twitter-auth-type"] = "OAuth2Session"
|
101
119
|
|
102
120
|
if auth:
|
103
121
|
if not self.account.auth_token:
|
@@ -108,6 +126,14 @@ class Client(BaseClient):
|
|
108
126
|
cookies["ct0"] = self.account.ct0
|
109
127
|
headers["x-csrf-token"] = self.account.ct0
|
110
128
|
|
129
|
+
# fmt: off
|
130
|
+
log_message = (f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
|
131
|
+
f" ==> Request {method} {url}")
|
132
|
+
if kwargs.get('data'): log_message += f"\nRequest data: {kwargs.get('data')}"
|
133
|
+
if kwargs.get('json'): log_message += f"\nRequest data: {kwargs.get('json')}"
|
134
|
+
logger.debug(log_message)
|
135
|
+
# fmt: on
|
136
|
+
|
111
137
|
try:
|
112
138
|
response = await self._session.request(method, url, **kwargs)
|
113
139
|
except requests.errors.RequestsError as exc:
|
@@ -120,17 +146,53 @@ class Client(BaseClient):
|
|
120
146
|
raise
|
121
147
|
|
122
148
|
data = response.text
|
123
|
-
|
149
|
+
# fmt: off
|
150
|
+
logger.debug(f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
|
151
|
+
f" <== Response {method} {url}"
|
152
|
+
f"\nStatus code: {response.status_code}"
|
153
|
+
f"\nResponse data: {data}")
|
154
|
+
# fmt: on
|
155
|
+
|
156
|
+
if ct0 := self._session.cookies.get("ct0", domain=".twitter.com"):
|
157
|
+
self.account.ct0 = ct0
|
158
|
+
|
159
|
+
auth_token = self._session.cookies.get("auth_token")
|
160
|
+
if auth_token and auth_token != self.account.auth_token:
|
161
|
+
self.account.auth_token = auth_token
|
162
|
+
logger.info(
|
163
|
+
f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
|
164
|
+
f" Requested new auth_token!"
|
165
|
+
)
|
166
|
+
|
167
|
+
try:
|
124
168
|
data = response.json()
|
169
|
+
except json.decoder.JSONDecodeError:
|
170
|
+
pass
|
125
171
|
|
126
|
-
if response.status_code
|
127
|
-
if
|
128
|
-
|
129
|
-
|
130
|
-
if
|
131
|
-
|
132
|
-
|
133
|
-
|
172
|
+
if 300 > response.status_code >= 200:
|
173
|
+
if isinstance(data, dict) and "errors" in data:
|
174
|
+
exc = HTTPException(response, data)
|
175
|
+
|
176
|
+
if 141 in exc.api_codes:
|
177
|
+
self.account.status = AccountStatus.SUSPENDED
|
178
|
+
raise Suspended(exc, self.account)
|
179
|
+
|
180
|
+
if 326 in exc.api_codes:
|
181
|
+
for error_data in exc.api_errors:
|
182
|
+
if (
|
183
|
+
error_data.get("code") == 326
|
184
|
+
and error_data.get("bounce_location")
|
185
|
+
== "/i/flow/consent_flow"
|
186
|
+
):
|
187
|
+
self.account.status = AccountStatus.CONSENT_LOCKED
|
188
|
+
raise ConsentLocked(exc, self.account)
|
189
|
+
|
190
|
+
self.account.status = AccountStatus.LOCKED
|
191
|
+
raise Locked(exc, self.account)
|
192
|
+
raise exc
|
193
|
+
|
194
|
+
self.account.status = AccountStatus.GOOD
|
195
|
+
return response, data
|
134
196
|
|
135
197
|
if response.status_code == 400:
|
136
198
|
raise BadRequest(response, data)
|
@@ -147,10 +209,6 @@ class Client(BaseClient):
|
|
147
209
|
if response.status_code == 403:
|
148
210
|
exc = Forbidden(response, data)
|
149
211
|
|
150
|
-
if 353 in exc.api_codes and "ct0" in response.cookies:
|
151
|
-
self.account.ct0 = response.cookies["ct0"]
|
152
|
-
return await self.request(method, url, auth, bearer, **kwargs)
|
153
|
-
|
154
212
|
if 64 in exc.api_codes:
|
155
213
|
self.account.status = AccountStatus.SUSPENDED
|
156
214
|
raise Suspended(exc, self.account)
|
@@ -165,52 +223,82 @@ class Client(BaseClient):
|
|
165
223
|
raise ConsentLocked(exc, self.account)
|
166
224
|
|
167
225
|
self.account.status = AccountStatus.LOCKED
|
168
|
-
|
169
|
-
raise Locked(exc, self.account)
|
170
|
-
|
171
|
-
await self.unlock()
|
172
|
-
return await self.request(method, url, auth, bearer, **kwargs)
|
226
|
+
raise Locked(exc, self.account)
|
173
227
|
|
174
228
|
raise exc
|
175
229
|
|
176
230
|
if response.status_code == 404:
|
177
231
|
raise NotFound(response, data)
|
178
232
|
|
233
|
+
if response.status_code == 429:
|
234
|
+
if wait_on_rate_limit is None:
|
235
|
+
wait_on_rate_limit = self.wait_on_rate_limit
|
236
|
+
if not wait_on_rate_limit:
|
237
|
+
raise RateLimited(response, data)
|
238
|
+
|
239
|
+
reset_time = int(response.headers["x-rate-limit-reset"])
|
240
|
+
sleep_time = reset_time - int(time()) + 1
|
241
|
+
if sleep_time > 0:
|
242
|
+
logger.info(
|
243
|
+
f"(auth_token={self.account.hidden_auth_token}, id={self.account.id}, username={self.account.username})"
|
244
|
+
f"Rate limited! Sleep time: {sleep_time} sec."
|
245
|
+
)
|
246
|
+
await asyncio.sleep(sleep_time)
|
247
|
+
return await self._request(
|
248
|
+
method,
|
249
|
+
url,
|
250
|
+
auth=auth,
|
251
|
+
bearer=bearer,
|
252
|
+
wait_on_rate_limit=wait_on_rate_limit,
|
253
|
+
**kwargs,
|
254
|
+
)
|
255
|
+
|
179
256
|
if response.status_code >= 500:
|
180
257
|
raise ServerError(response, data)
|
181
258
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
259
|
+
async def request(
|
260
|
+
self,
|
261
|
+
method,
|
262
|
+
url,
|
263
|
+
*,
|
264
|
+
auto_unlock: bool = True,
|
265
|
+
auto_relogin: bool = None,
|
266
|
+
**kwargs,
|
267
|
+
) -> tuple[requests.Response, Any]:
|
268
|
+
try:
|
269
|
+
return await self._request(method, url, **kwargs)
|
187
270
|
|
188
|
-
|
189
|
-
|
190
|
-
raise
|
271
|
+
except Locked:
|
272
|
+
if not self.capsolver_api_key or not auto_unlock:
|
273
|
+
raise
|
191
274
|
|
192
|
-
|
193
|
-
|
194
|
-
if (
|
195
|
-
error_data.get("code") == 326
|
196
|
-
and error_data.get("bounce_location") == "/i/flow/consent_flow"
|
197
|
-
):
|
198
|
-
self.account.status = AccountStatus.CONSENT_LOCKED
|
199
|
-
raise ConsentLocked(exc, self.account)
|
275
|
+
await self.unlock()
|
276
|
+
return await self._request(method, url, **kwargs)
|
200
277
|
|
201
|
-
|
202
|
-
|
203
|
-
|
278
|
+
except BadToken:
|
279
|
+
if auto_relogin is None:
|
280
|
+
auto_relogin = self.auto_relogin
|
281
|
+
if (
|
282
|
+
not auto_relogin
|
283
|
+
or not self.account.password
|
284
|
+
or not (self.account.email or self.account.username)
|
285
|
+
):
|
286
|
+
raise
|
204
287
|
|
205
|
-
|
206
|
-
|
288
|
+
await self.relogin()
|
289
|
+
return await self._request(method, url, **kwargs)
|
207
290
|
|
208
|
-
|
291
|
+
except Forbidden as exc:
|
292
|
+
if 353 in exc.api_codes and "ct0" in exc.response.cookies:
|
293
|
+
return await self._request(method, url, **kwargs)
|
294
|
+
else:
|
295
|
+
raise
|
209
296
|
|
210
|
-
|
211
|
-
|
297
|
+
async def on_startup(self):
|
298
|
+
if self.request_self_on_startup:
|
299
|
+
await self.request_user()
|
212
300
|
|
213
|
-
async def
|
301
|
+
async def _request_oauth2_auth_code(
|
214
302
|
self,
|
215
303
|
client_id: str,
|
216
304
|
code_challenge: str,
|
@@ -234,7 +322,7 @@ class Client(BaseClient):
|
|
234
322
|
auth_code = response_json["auth_code"]
|
235
323
|
return auth_code
|
236
324
|
|
237
|
-
async def
|
325
|
+
async def _confirm_oauth2(self, auth_code: str):
|
238
326
|
data = {
|
239
327
|
"approval": "true",
|
240
328
|
"code": auth_code,
|
@@ -247,7 +335,7 @@ class Client(BaseClient):
|
|
247
335
|
data=data,
|
248
336
|
)
|
249
337
|
|
250
|
-
async def
|
338
|
+
async def oauth2(
|
251
339
|
self,
|
252
340
|
client_id: str,
|
253
341
|
code_challenge: str,
|
@@ -263,15 +351,13 @@ class Client(BaseClient):
|
|
263
351
|
Привязка (бинд, линк) приложения.
|
264
352
|
|
265
353
|
:param client_id: Идентификатор клиента, используемый для OAuth.
|
266
|
-
:param code_challenge: Код-вызов, используемый для PKCE (Proof Key for Code Exchange).
|
267
354
|
:param state: Уникальная строка состояния для предотвращения CSRF-атак.
|
268
355
|
:param redirect_uri: URI перенаправления, на который будет отправлен ответ.
|
269
|
-
:param code_challenge_method: Метод, используемый для преобразования code_verifier в code_challenge.
|
270
356
|
:param scope: Строка областей доступа, запрашиваемых у пользователя.
|
271
357
|
:param response_type: Тип ответа, который ожидается от сервера авторизации.
|
272
358
|
:return: Код авторизации (привязки).
|
273
359
|
"""
|
274
|
-
auth_code = await self.
|
360
|
+
auth_code = await self._request_oauth2_auth_code(
|
275
361
|
client_id,
|
276
362
|
code_challenge,
|
277
363
|
state,
|
@@ -280,7 +366,7 @@ class Client(BaseClient):
|
|
280
366
|
scope,
|
281
367
|
response_type,
|
282
368
|
)
|
283
|
-
await self.
|
369
|
+
await self._confirm_oauth2(auth_code)
|
284
370
|
return auth_code
|
285
371
|
|
286
372
|
async def _oauth(self, oauth_token: str, **oauth_params) -> requests.Response:
|
@@ -335,12 +421,12 @@ class Client(BaseClient):
|
|
335
421
|
|
336
422
|
return authenticity_token, redirect_url
|
337
423
|
|
338
|
-
async def
|
424
|
+
async def request_and_set_username(self):
|
339
425
|
url = "https://twitter.com/i/api/1.1/account/settings.json"
|
340
426
|
response, response_json = await self.request("POST", url)
|
341
427
|
self.account.username = response_json["screen_name"]
|
342
428
|
|
343
|
-
async def
|
429
|
+
async def _request_user(self, username: str) -> User:
|
344
430
|
url, query_id = self._action_to_url("UserByScreenName")
|
345
431
|
username = remove_at_sign(username)
|
346
432
|
variables = {
|
@@ -367,48 +453,56 @@ class Client(BaseClient):
|
|
367
453
|
"features": to_json(features),
|
368
454
|
"fieldToggles": to_json(field_toggles),
|
369
455
|
}
|
370
|
-
response,
|
371
|
-
|
456
|
+
response, data = await self.request("GET", url, params=params)
|
457
|
+
return User.from_raw_data(data["data"]["user"]["result"])
|
372
458
|
|
373
|
-
|
374
|
-
|
375
|
-
|
459
|
+
async def request_user(
|
460
|
+
self, *, username: str = None, user_id: int | str = None
|
461
|
+
) -> User | Account:
|
462
|
+
if username and user_id:
|
463
|
+
raise ValueError("Specify username or user_id, not both.")
|
376
464
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
if username:
|
381
|
-
return await self._request_user_data(username)
|
465
|
+
if user_id:
|
466
|
+
users = await self.request_users((user_id,))
|
467
|
+
user = users[user_id]
|
382
468
|
else:
|
383
|
-
if not
|
384
|
-
|
385
|
-
|
469
|
+
if not username:
|
470
|
+
if not self.account.username:
|
471
|
+
await self.request_and_set_username()
|
472
|
+
username = self.account.username
|
473
|
+
|
474
|
+
user = await self._request_user(username)
|
475
|
+
|
476
|
+
if self.account.username == user.username:
|
477
|
+
self.account.update(**user.model_dump())
|
478
|
+
user = self.account
|
479
|
+
|
480
|
+
return user
|
386
481
|
|
387
482
|
async def upload_image(
|
388
483
|
self,
|
389
484
|
image: bytes,
|
390
485
|
attempts: int = 3,
|
391
486
|
timeout: float | tuple[float, float] = 10,
|
392
|
-
) ->
|
487
|
+
) -> Media:
|
393
488
|
"""
|
394
489
|
Upload image as bytes.
|
395
490
|
|
396
491
|
Иногда при первой попытке загрузки изображения возвращает 408,
|
397
492
|
после чего повторная попытка загрузки изображения проходит успешно
|
398
493
|
|
399
|
-
:return: Media
|
494
|
+
:return: Media
|
400
495
|
"""
|
401
496
|
url = "https://upload.twitter.com/1.1/media/upload.json"
|
402
497
|
|
403
|
-
|
498
|
+
payload = {"media_data": base64.b64encode(image)}
|
404
499
|
|
405
500
|
for attempt in range(attempts):
|
406
501
|
try:
|
407
|
-
response,
|
408
|
-
"POST", url, data=
|
502
|
+
response, data = await self.request(
|
503
|
+
"POST", url, data=payload, timeout=timeout
|
409
504
|
)
|
410
|
-
|
411
|
-
return media_id
|
505
|
+
return Media(**data)
|
412
506
|
except (HTTPException, requests.errors.RequestsError) as exc:
|
413
507
|
if (
|
414
508
|
attempt < attempts - 1
|
@@ -425,9 +519,6 @@ class Client(BaseClient):
|
|
425
519
|
else:
|
426
520
|
raise
|
427
521
|
|
428
|
-
media_id = response_json["media_id"]
|
429
|
-
return media_id
|
430
|
-
|
431
522
|
async def _follow_action(self, action: str, user_id: int | str) -> bool:
|
432
523
|
url = f"https://twitter.com/i/api/1.1/friendships/{action}.json"
|
433
524
|
params = {
|
@@ -466,22 +557,60 @@ class Client(BaseClient):
|
|
466
557
|
"variables": {"tweet_id": tweet_id, "dark_request": False},
|
467
558
|
"queryId": query_id,
|
468
559
|
}
|
469
|
-
response,
|
470
|
-
return
|
560
|
+
response, data = await self.request("POST", url, json=json_payload)
|
561
|
+
return data
|
471
562
|
|
472
|
-
async def
|
563
|
+
async def _repost(self, tweet_id: int | str) -> Tweet:
|
564
|
+
data = await self._interact_with_tweet("CreateRetweet", tweet_id)
|
565
|
+
tweet_id = data["data"]["create_retweet"]["retweet_results"]["result"]["rest_id"] # type: ignore
|
566
|
+
return await self.request_tweet(tweet_id)
|
567
|
+
|
568
|
+
async def _repost_or_search_duplicate(
|
569
|
+
self,
|
570
|
+
tweet_id: int,
|
571
|
+
*,
|
572
|
+
search_duplicate: bool = True,
|
573
|
+
) -> Tweet:
|
574
|
+
try:
|
575
|
+
tweet = await self._repost(tweet_id)
|
576
|
+
except HTTPException as exc:
|
577
|
+
if (
|
578
|
+
search_duplicate
|
579
|
+
and 327
|
580
|
+
in exc.api_codes # duplicate retweet (You have already retweeted this Tweet)
|
581
|
+
):
|
582
|
+
tweets = await self.request_tweets(self.account.id)
|
583
|
+
duplicate_tweet = None
|
584
|
+
for tweet_ in tweets: # type: Tweet
|
585
|
+
if tweet_.retweeted_tweet and tweet_.retweeted_tweet.id == tweet_id:
|
586
|
+
duplicate_tweet = tweet_
|
587
|
+
|
588
|
+
if not duplicate_tweet:
|
589
|
+
raise FailedToFindDuplicatePost(
|
590
|
+
f"Couldn't find a post duplicate in the next 20 posts"
|
591
|
+
)
|
592
|
+
|
593
|
+
tweet = duplicate_tweet
|
594
|
+
|
595
|
+
else:
|
596
|
+
raise
|
597
|
+
|
598
|
+
return tweet
|
599
|
+
|
600
|
+
async def repost(
|
601
|
+
self,
|
602
|
+
tweet_id: int,
|
603
|
+
*,
|
604
|
+
search_duplicate: bool = True,
|
605
|
+
) -> Tweet:
|
473
606
|
"""
|
474
607
|
Repost (retweet)
|
475
608
|
|
476
|
-
:return: Tweet
|
609
|
+
:return: Tweet
|
477
610
|
"""
|
478
|
-
|
479
|
-
|
480
|
-
response_json["data"]["create_retweet"]["retweet_results"]["result"][
|
481
|
-
"rest_id"
|
482
|
-
]
|
611
|
+
return await self._repost_or_search_duplicate(
|
612
|
+
tweet_id, search_duplicate=search_duplicate
|
483
613
|
)
|
484
|
-
return retweet_id
|
485
614
|
|
486
615
|
async def like(self, tweet_id: int) -> bool:
|
487
616
|
response_json = await self._interact_with_tweet("FavoriteTweet", tweet_id)
|
@@ -588,7 +717,6 @@ class Client(BaseClient):
|
|
588
717
|
tweet_id_to_reply: str | int = None,
|
589
718
|
attachment_url: str = None,
|
590
719
|
search_duplicate: bool = True,
|
591
|
-
with_tweet_url: bool = True,
|
592
720
|
) -> Tweet:
|
593
721
|
try:
|
594
722
|
tweet = await self._tweet(
|
@@ -602,10 +730,10 @@ class Client(BaseClient):
|
|
602
730
|
search_duplicate
|
603
731
|
and 187 in exc.api_codes # duplicate tweet (Status is a duplicate)
|
604
732
|
):
|
605
|
-
tweets = await self.request_tweets(
|
733
|
+
tweets = await self.request_tweets()
|
606
734
|
duplicate_tweet = None
|
607
735
|
for tweet_ in tweets:
|
608
|
-
if tweet_.
|
736
|
+
if tweet_.text.startswith(text.strip()):
|
609
737
|
duplicate_tweet = tweet_
|
610
738
|
|
611
739
|
if not duplicate_tweet:
|
@@ -617,11 +745,6 @@ class Client(BaseClient):
|
|
617
745
|
else:
|
618
746
|
raise
|
619
747
|
|
620
|
-
if with_tweet_url:
|
621
|
-
if not self.account.username:
|
622
|
-
await self.request_user_data()
|
623
|
-
tweet.url = create_tweet_url(self.account.username, tweet.id)
|
624
|
-
|
625
748
|
return tweet
|
626
749
|
|
627
750
|
async def tweet(
|
@@ -630,13 +753,11 @@ class Client(BaseClient):
|
|
630
753
|
*,
|
631
754
|
media_id: int | str = None,
|
632
755
|
search_duplicate: bool = True,
|
633
|
-
with_tweet_url: bool = True,
|
634
756
|
) -> Tweet:
|
635
757
|
return await self._tweet_or_search_duplicate(
|
636
758
|
text,
|
637
759
|
media_id=media_id,
|
638
760
|
search_duplicate=search_duplicate,
|
639
|
-
with_tweet_url=with_tweet_url,
|
640
761
|
)
|
641
762
|
|
642
763
|
async def reply(
|
@@ -646,14 +767,12 @@ class Client(BaseClient):
|
|
646
767
|
*,
|
647
768
|
media_id: int | str = None,
|
648
769
|
search_duplicate: bool = True,
|
649
|
-
with_tweet_url: bool = True,
|
650
770
|
) -> Tweet:
|
651
771
|
return await self._tweet_or_search_duplicate(
|
652
772
|
text,
|
653
773
|
media_id=media_id,
|
654
774
|
tweet_id_to_reply=tweet_id,
|
655
775
|
search_duplicate=search_duplicate,
|
656
|
-
with_tweet_url=with_tweet_url,
|
657
776
|
)
|
658
777
|
|
659
778
|
async def quote(
|
@@ -663,14 +782,12 @@ class Client(BaseClient):
|
|
663
782
|
*,
|
664
783
|
media_id: int | str = None,
|
665
784
|
search_duplicate: bool = True,
|
666
|
-
with_tweet_url: bool = True,
|
667
785
|
) -> Tweet:
|
668
786
|
return await self._tweet_or_search_duplicate(
|
669
787
|
text,
|
670
788
|
media_id=media_id,
|
671
789
|
attachment_url=tweet_url,
|
672
790
|
search_duplicate=search_duplicate,
|
673
|
-
with_tweet_url=with_tweet_url,
|
674
791
|
)
|
675
792
|
|
676
793
|
async def vote(
|
@@ -692,7 +809,7 @@ class Client(BaseClient):
|
|
692
809
|
|
693
810
|
async def _request_users(
|
694
811
|
self, action: str, user_id: int | str, count: int
|
695
|
-
) -> list[
|
812
|
+
) -> list[User]:
|
696
813
|
url, query_id = self._action_to_url(action)
|
697
814
|
variables = {
|
698
815
|
"userId": str(user_id),
|
@@ -737,12 +854,12 @@ class Client(BaseClient):
|
|
737
854
|
user_data_dict = entry["content"]["itemContent"]["user_results"][
|
738
855
|
"result"
|
739
856
|
]
|
740
|
-
users.append(
|
857
|
+
users.append(User.from_raw_data(user_data_dict))
|
741
858
|
return users
|
742
859
|
|
743
860
|
async def request_followers(
|
744
861
|
self, user_id: int | str = None, count: int = 10
|
745
|
-
) -> list[
|
862
|
+
) -> list[User]:
|
746
863
|
"""
|
747
864
|
:param user_id: Текущий пользователь, если не передан ID иного пользователя.
|
748
865
|
:param count: Количество подписчиков.
|
@@ -751,12 +868,12 @@ class Client(BaseClient):
|
|
751
868
|
return await self._request_users("Followers", user_id, count)
|
752
869
|
else:
|
753
870
|
if not self.account.id:
|
754
|
-
await self.
|
871
|
+
await self.request_user()
|
755
872
|
return await self._request_users("Followers", self.account.id, count)
|
756
873
|
|
757
874
|
async def request_followings(
|
758
875
|
self, user_id: int | str = None, count: int = 10
|
759
|
-
) -> list[
|
876
|
+
) -> list[User]:
|
760
877
|
"""
|
761
878
|
:param user_id: Текущий пользователь, если не передан ID иного пользователя.
|
762
879
|
:param count: Количество подписчиков.
|
@@ -765,12 +882,32 @@ class Client(BaseClient):
|
|
765
882
|
return await self._request_users("Following", user_id, count)
|
766
883
|
else:
|
767
884
|
if not self.account.id:
|
768
|
-
await self.
|
885
|
+
await self.request_user()
|
769
886
|
return await self._request_users("Following", self.account.id, count)
|
770
887
|
|
771
|
-
async def
|
772
|
-
|
773
|
-
|
888
|
+
async def request_users(
|
889
|
+
self, user_ids: Iterable[str | int]
|
890
|
+
) -> dict[int : User | Account]:
|
891
|
+
url, query_id = self._action_to_url("UsersByRestIds")
|
892
|
+
variables = {"userIds": list({str(user_id) for user_id in user_ids})}
|
893
|
+
features = {
|
894
|
+
"responsive_web_graphql_exclude_directive_enabled": True,
|
895
|
+
"responsive_web_graphql_skip_user_profile_image_extensions_enabled": False,
|
896
|
+
"responsive_web_graphql_timeline_navigation_enabled": True,
|
897
|
+
"verified_phone_label_enabled": False,
|
898
|
+
}
|
899
|
+
query = {"variables": variables, "features": features}
|
900
|
+
response, data = await self.request("GET", url, params=query)
|
901
|
+
|
902
|
+
users = {}
|
903
|
+
for user_data in data["data"]["users"]:
|
904
|
+
user_data = user_data["result"]
|
905
|
+
user = User.from_raw_data(user_data)
|
906
|
+
users[user.id] = user
|
907
|
+
return users
|
908
|
+
|
909
|
+
async def _request_tweet(self, tweet_id: int | str) -> Tweet:
|
910
|
+
url, query_id = self._action_to_url("TweetDetail")
|
774
911
|
variables = {
|
775
912
|
"focalTweetId": str(tweet_id),
|
776
913
|
"with_rux_injections": False,
|
@@ -801,12 +938,71 @@ class Client(BaseClient):
|
|
801
938
|
"longform_notetweets_inline_media_enabled": True,
|
802
939
|
"responsive_web_enhance_cards_enabled": False,
|
803
940
|
}
|
804
|
-
|
941
|
+
query = {
|
805
942
|
"variables": to_json(variables),
|
806
943
|
"features": to_json(features),
|
807
944
|
}
|
808
|
-
response,
|
809
|
-
|
945
|
+
response, data = await self.request("GET", url, params=query)
|
946
|
+
instructions = data["data"]["threaded_conversation_with_injections_v2"]["instructions"] # type: ignore
|
947
|
+
tweet_data = tweets_data_from_instructions(instructions)[0]
|
948
|
+
return Tweet.from_raw_data(tweet_data)
|
949
|
+
|
950
|
+
async def _request_tweets(self, user_id: int | str, count: int = 20) -> list[Tweet]:
|
951
|
+
url, query_id = self._action_to_url("UserTweets")
|
952
|
+
variables = {
|
953
|
+
"userId": str(user_id),
|
954
|
+
"count": count,
|
955
|
+
"includePromotedContent": True,
|
956
|
+
"withQuickPromoteEligibilityTweetFields": True,
|
957
|
+
"withVoice": True,
|
958
|
+
"withV2Timeline": True,
|
959
|
+
}
|
960
|
+
features = {
|
961
|
+
"responsive_web_graphql_exclude_directive_enabled": True,
|
962
|
+
"verified_phone_label_enabled": False,
|
963
|
+
"creator_subscriptions_tweet_preview_api_enabled": True,
|
964
|
+
"responsive_web_graphql_timeline_navigation_enabled": True,
|
965
|
+
"responsive_web_graphql_skip_user_profile_image_extensions_enabled": False,
|
966
|
+
"c9s_tweet_anatomy_moderator_badge_enabled": True,
|
967
|
+
"tweetypie_unmention_optimization_enabled": True,
|
968
|
+
"responsive_web_edit_tweet_api_enabled": True,
|
969
|
+
"graphql_is_translatable_rweb_tweet_is_translatable_enabled": True,
|
970
|
+
"view_counts_everywhere_api_enabled": True,
|
971
|
+
"longform_notetweets_consumption_enabled": True,
|
972
|
+
"responsive_web_twitter_article_tweet_consumption_enabled": False,
|
973
|
+
"tweet_awards_web_tipping_enabled": False,
|
974
|
+
"freedom_of_speech_not_reach_fetch_enabled": True,
|
975
|
+
"standardized_nudges_misinfo": True,
|
976
|
+
"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": True,
|
977
|
+
"rweb_video_timestamps_enabled": True,
|
978
|
+
"longform_notetweets_rich_text_read_enabled": True,
|
979
|
+
"longform_notetweets_inline_media_enabled": True,
|
980
|
+
"responsive_web_media_download_video_enabled": False,
|
981
|
+
"responsive_web_enhance_cards_enabled": False,
|
982
|
+
}
|
983
|
+
params = {"variables": to_json(variables), "features": to_json(features)}
|
984
|
+
response, data = await self.request("GET", url, params=params)
|
985
|
+
|
986
|
+
instructions = data["data"]["user"]["result"]["timeline_v2"]["timeline"][
|
987
|
+
"instructions"
|
988
|
+
]
|
989
|
+
tweets_data = tweets_data_from_instructions(instructions)
|
990
|
+
return [Tweet.from_raw_data(tweet_data) for tweet_data in tweets_data]
|
991
|
+
|
992
|
+
async def request_tweet(self, tweet_id: int | str) -> Tweet:
|
993
|
+
return await self._request_tweet(tweet_id)
|
994
|
+
|
995
|
+
async def request_tweets(
|
996
|
+
self,
|
997
|
+
user_id: int | str = None,
|
998
|
+
count: int = 20,
|
999
|
+
) -> list[Tweet]:
|
1000
|
+
if not user_id:
|
1001
|
+
if not self.account.id:
|
1002
|
+
await self.request_user()
|
1003
|
+
user_id = self.account.id
|
1004
|
+
|
1005
|
+
return await self._request_tweets(user_id, count)
|
810
1006
|
|
811
1007
|
async def _update_profile_image(
|
812
1008
|
self, type: Literal["banner", "image"], media_id: str | int
|
@@ -832,8 +1028,8 @@ class Client(BaseClient):
|
|
832
1028
|
"skip_status": "1",
|
833
1029
|
"return_user": "true",
|
834
1030
|
}
|
835
|
-
response,
|
836
|
-
image_url =
|
1031
|
+
response, data = await self.request("POST", url, params=params)
|
1032
|
+
image_url = data[f"profile_{type}_url"]
|
837
1033
|
return image_url
|
838
1034
|
|
839
1035
|
async def update_profile_avatar(self, media_id: int | str) -> str:
|
@@ -850,12 +1046,12 @@ class Client(BaseClient):
|
|
850
1046
|
|
851
1047
|
async def change_username(self, username: str) -> bool:
|
852
1048
|
url = "https://twitter.com/i/api/1.1/account/settings.json"
|
853
|
-
|
854
|
-
response,
|
855
|
-
new_username =
|
856
|
-
|
1049
|
+
payload = {"screen_name": username}
|
1050
|
+
response, data = await self.request("POST", url, data=payload)
|
1051
|
+
new_username = data["screen_name"]
|
1052
|
+
changed = new_username == username
|
857
1053
|
self.account.username = new_username
|
858
|
-
return
|
1054
|
+
return changed
|
859
1055
|
|
860
1056
|
async def change_password(self, password: str) -> bool:
|
861
1057
|
"""
|
@@ -865,17 +1061,18 @@ class Client(BaseClient):
|
|
865
1061
|
raise ValueError(f"Specify the current password before changing it")
|
866
1062
|
|
867
1063
|
url = "https://twitter.com/i/api/i/account/change_password.json"
|
868
|
-
|
1064
|
+
payload = {
|
869
1065
|
"current_password": self.account.password,
|
870
1066
|
"password": password,
|
871
1067
|
"password_confirmation": password,
|
872
1068
|
}
|
873
|
-
response,
|
874
|
-
|
1069
|
+
response, data = await self.request("POST", url, data=payload)
|
1070
|
+
changed = data["status"] == "ok"
|
1071
|
+
# TODO Делать это автоматически в методе request
|
875
1072
|
auth_token = response.cookies.get("auth_token", domain=".twitter.com")
|
876
1073
|
self.account.auth_token = auth_token
|
877
1074
|
self.account.password = password
|
878
|
-
return
|
1075
|
+
return changed
|
879
1076
|
|
880
1077
|
async def update_profile(
|
881
1078
|
self,
|
@@ -891,9 +1088,9 @@ class Client(BaseClient):
|
|
891
1088
|
raise ValueError("Specify at least one param")
|
892
1089
|
|
893
1090
|
url = "https://twitter.com/i/api/1.1/account/update_profile.json"
|
894
|
-
headers = {"content-type": "application/x-www-form-urlencoded"}
|
1091
|
+
# headers = {"content-type": "application/x-www-form-urlencoded"}
|
895
1092
|
# Создаем словарь data, включая в него только те ключи, для которых значения не равны None
|
896
|
-
|
1093
|
+
payload = {
|
897
1094
|
k: v
|
898
1095
|
for k, v in [
|
899
1096
|
("name", name),
|
@@ -903,26 +1100,23 @@ class Client(BaseClient):
|
|
903
1100
|
]
|
904
1101
|
if v is not None
|
905
1102
|
}
|
906
|
-
response, response_json = await self.request(
|
907
|
-
|
908
|
-
)
|
1103
|
+
# response, response_json = await self.request("POST", url, headers=headers, data=payload)
|
1104
|
+
response, data = await self.request("POST", url, data=payload)
|
909
1105
|
# Проверяем, что все переданные параметры соответствуют полученным
|
910
|
-
|
911
|
-
|
912
|
-
for key, value in data.items()
|
913
|
-
if key != "url"
|
1106
|
+
updated = all(
|
1107
|
+
data.get(key) == value for key, value in payload.items() if key != "url"
|
914
1108
|
)
|
915
1109
|
if website:
|
916
|
-
|
917
|
-
|
1110
|
+
updated &= URL(website) == URL(
|
1111
|
+
data["entities"]["url"]["urls"][0]["expanded_url"]
|
918
1112
|
)
|
919
|
-
await self.
|
920
|
-
return
|
1113
|
+
await self.request_user()
|
1114
|
+
return updated
|
921
1115
|
|
922
1116
|
async def establish_status(self):
|
923
1117
|
url = "https://twitter.com/i/api/1.1/account/update_profile.json"
|
924
1118
|
try:
|
925
|
-
await self.request("POST", url)
|
1119
|
+
await self.request("POST", url, auto_unlock=False, auto_relogin=False)
|
926
1120
|
except BadAccount:
|
927
1121
|
pass
|
928
1122
|
|
@@ -947,7 +1141,7 @@ class Client(BaseClient):
|
|
947
1141
|
"POST", url, headers=headers, data=data
|
948
1142
|
)
|
949
1143
|
birthdate_data = response_json["extended_profile"]["birthdate"]
|
950
|
-
|
1144
|
+
updated = all(
|
951
1145
|
(
|
952
1146
|
birthdate_data["day"] == day,
|
953
1147
|
birthdate_data["month"] == month,
|
@@ -956,7 +1150,7 @@ class Client(BaseClient):
|
|
956
1150
|
birthdate_data["year_visibility"] == year_visibility,
|
957
1151
|
)
|
958
1152
|
)
|
959
|
-
return
|
1153
|
+
return updated
|
960
1154
|
|
961
1155
|
async def send_message(self, user_id: int | str, text: str) -> dict:
|
962
1156
|
"""
|
@@ -972,6 +1166,20 @@ class Client(BaseClient):
|
|
972
1166
|
},
|
973
1167
|
}
|
974
1168
|
}
|
1169
|
+
response, data = await self.request("POST", url, json=payload)
|
1170
|
+
event_data = data["event"]
|
1171
|
+
return event_data # TODO Возвращать модель, а не словарь
|
1172
|
+
|
1173
|
+
async def send_message_to_conversation(
|
1174
|
+
self, conversation_id: int | str, text: str
|
1175
|
+
) -> dict:
|
1176
|
+
"""
|
1177
|
+
requires OAuth1 or OAuth2
|
1178
|
+
|
1179
|
+
:return: Event data
|
1180
|
+
"""
|
1181
|
+
url = f"https://api.twitter.com/2/dm_conversations/{conversation_id}/messages"
|
1182
|
+
payload = {"text": text}
|
975
1183
|
response, response_json = await self.request("POST", url, json=payload)
|
976
1184
|
event_data = response_json["event"]
|
977
1185
|
return event_data
|
@@ -1023,56 +1231,7 @@ class Client(BaseClient):
|
|
1023
1231
|
for entry in response_json["inbox_initial_state"]["entries"]
|
1024
1232
|
if "message" in entry
|
1025
1233
|
]
|
1026
|
-
return messages
|
1027
|
-
|
1028
|
-
async def request_tweets(self, user_id: str | int, count: int = 20) -> list[Tweet]:
|
1029
|
-
url, query_id = self._action_to_url("UserTweets")
|
1030
|
-
variables = {
|
1031
|
-
"userId": str(user_id),
|
1032
|
-
"count": count,
|
1033
|
-
"includePromotedContent": True,
|
1034
|
-
"withQuickPromoteEligibilityTweetFields": True,
|
1035
|
-
"withVoice": True,
|
1036
|
-
"withV2Timeline": True,
|
1037
|
-
}
|
1038
|
-
features = {
|
1039
|
-
"responsive_web_graphql_exclude_directive_enabled": True,
|
1040
|
-
"verified_phone_label_enabled": False,
|
1041
|
-
"creator_subscriptions_tweet_preview_api_enabled": True,
|
1042
|
-
"responsive_web_graphql_timeline_navigation_enabled": True,
|
1043
|
-
"responsive_web_graphql_skip_user_profile_image_extensions_enabled": False,
|
1044
|
-
"c9s_tweet_anatomy_moderator_badge_enabled": True,
|
1045
|
-
"tweetypie_unmention_optimization_enabled": True,
|
1046
|
-
"responsive_web_edit_tweet_api_enabled": True,
|
1047
|
-
"graphql_is_translatable_rweb_tweet_is_translatable_enabled": True,
|
1048
|
-
"view_counts_everywhere_api_enabled": True,
|
1049
|
-
"longform_notetweets_consumption_enabled": True,
|
1050
|
-
"responsive_web_twitter_article_tweet_consumption_enabled": False,
|
1051
|
-
"tweet_awards_web_tipping_enabled": False,
|
1052
|
-
"freedom_of_speech_not_reach_fetch_enabled": True,
|
1053
|
-
"standardized_nudges_misinfo": True,
|
1054
|
-
"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": True,
|
1055
|
-
"rweb_video_timestamps_enabled": True,
|
1056
|
-
"longform_notetweets_rich_text_read_enabled": True,
|
1057
|
-
"longform_notetweets_inline_media_enabled": True,
|
1058
|
-
"responsive_web_media_download_video_enabled": False,
|
1059
|
-
"responsive_web_enhance_cards_enabled": False,
|
1060
|
-
}
|
1061
|
-
params = {"variables": to_json(variables), "features": to_json(features)}
|
1062
|
-
response, response_json = await self.request("GET", url, params=params)
|
1063
|
-
|
1064
|
-
tweets = []
|
1065
|
-
for instruction in response_json["data"]["user"]["result"]["timeline_v2"][
|
1066
|
-
"timeline"
|
1067
|
-
]["instructions"]:
|
1068
|
-
if instruction["type"] == "TimelineAddEntries":
|
1069
|
-
for entry in instruction["entries"]:
|
1070
|
-
if entry["entryId"].startswith("tweet"):
|
1071
|
-
tweet_data = entry["content"]["itemContent"]["tweet_results"][
|
1072
|
-
"result"
|
1073
|
-
]
|
1074
|
-
tweets.append(Tweet.from_raw_data(tweet_data))
|
1075
|
-
return tweets
|
1234
|
+
return messages # TODO Возвращать модели, а не словари
|
1076
1235
|
|
1077
1236
|
async def _confirm_unlock(
|
1078
1237
|
self,
|
@@ -1133,8 +1292,19 @@ class Client(BaseClient):
|
|
1133
1292
|
else:
|
1134
1293
|
funcaptcha["captcha_type"] = FunCaptchaTypeEnm.FunCaptchaTaskProxyLess
|
1135
1294
|
|
1136
|
-
while needs_unlock:
|
1295
|
+
while needs_unlock and attempt <= self.max_unlock_attempts:
|
1137
1296
|
solution = await FunCaptcha(**funcaptcha).aio_captcha_handler()
|
1297
|
+
if solution.errorId:
|
1298
|
+
logger.warning(
|
1299
|
+
f"{self.account} Failed to solve funcaptcha:"
|
1300
|
+
f"\n\tUnlock attempt: {attempt}/{self.max_unlock_attempts}"
|
1301
|
+
f"\n\tError ID: {solution.errorId}"
|
1302
|
+
f"\n\tError code: {solution.errorCode}"
|
1303
|
+
f"\n\tError description: {solution.errorDescription}"
|
1304
|
+
)
|
1305
|
+
attempt += 1
|
1306
|
+
continue
|
1307
|
+
|
1138
1308
|
token = solution.solution["token"]
|
1139
1309
|
response, html = await self._confirm_unlock(
|
1140
1310
|
authenticity_token,
|
@@ -1142,12 +1312,8 @@ class Client(BaseClient):
|
|
1142
1312
|
verification_string=token,
|
1143
1313
|
)
|
1144
1314
|
|
1145
|
-
if
|
1146
|
-
|
1147
|
-
or response.url == "https://twitter.com/?lang=en"
|
1148
|
-
):
|
1149
|
-
await self.establish_status()
|
1150
|
-
return
|
1315
|
+
if response.url == "https://twitter.com/?lang=en":
|
1316
|
+
break
|
1151
1317
|
|
1152
1318
|
(
|
1153
1319
|
authenticity_token,
|
@@ -1171,6 +1337,8 @@ class Client(BaseClient):
|
|
1171
1337
|
|
1172
1338
|
attempt += 1
|
1173
1339
|
|
1340
|
+
await self.establish_status()
|
1341
|
+
|
1174
1342
|
async def _task(self, **kwargs):
|
1175
1343
|
"""
|
1176
1344
|
:return: flow_token, subtasks
|
@@ -1335,7 +1503,7 @@ class Client(BaseClient):
|
|
1335
1503
|
"fieldToggles": to_json(field_toggles),
|
1336
1504
|
"variables": to_json(variables),
|
1337
1505
|
}
|
1338
|
-
return self.request("GET", url, params=params)
|
1506
|
+
return await self.request("GET", url, params=params)
|
1339
1507
|
|
1340
1508
|
async def _request_guest_token(self) -> str:
|
1341
1509
|
"""
|
@@ -1374,18 +1542,9 @@ class Client(BaseClient):
|
|
1374
1542
|
flow_token
|
1375
1543
|
)
|
1376
1544
|
|
1377
|
-
# TODO Возможно, стоит добавить отслеживание этих параметров прямо в request
|
1378
|
-
self.account.auth_token = self._session.cookies["auth_token"]
|
1379
|
-
self.account.ct0 = self._session.cookies["ct0"]
|
1380
|
-
|
1381
1545
|
await self._finish_task(flow_token)
|
1382
1546
|
|
1383
|
-
async def
|
1384
|
-
if self.account.auth_token:
|
1385
|
-
await self.establish_status()
|
1386
|
-
if self.account.status != "BAD_TOKEN":
|
1387
|
-
return
|
1388
|
-
|
1547
|
+
async def relogin(self):
|
1389
1548
|
if not self.account.email and not self.account.username:
|
1390
1549
|
raise ValueError("No email or username")
|
1391
1550
|
|
@@ -1393,11 +1552,20 @@ class Client(BaseClient):
|
|
1393
1552
|
raise ValueError("No password")
|
1394
1553
|
|
1395
1554
|
await self._login()
|
1555
|
+
await self._viewer()
|
1396
1556
|
await self.establish_status()
|
1397
1557
|
|
1558
|
+
async def login(self):
|
1559
|
+
if self.account.auth_token:
|
1560
|
+
await self.establish_status()
|
1561
|
+
if self.account.status not in ("BAD_TOKEN", "CONSENT_LOCKED"):
|
1562
|
+
return
|
1563
|
+
|
1564
|
+
await self.relogin()
|
1565
|
+
|
1398
1566
|
async def totp_is_enabled(self):
|
1399
1567
|
if not self.account.id:
|
1400
|
-
await self.
|
1568
|
+
await self.request_user()
|
1401
1569
|
|
1402
1570
|
url = f"https://twitter.com/i/api/1.1/strato/column/User/{self.account.id}/account-security/twoFactorAuthSettings2"
|
1403
1571
|
response, data = await self.request("GET", url)
|
@@ -1409,7 +1577,7 @@ class Client(BaseClient):
|
|
1409
1577
|
"""
|
1410
1578
|
:return: flow_token, subtask_ids
|
1411
1579
|
"""
|
1412
|
-
|
1580
|
+
query = {
|
1413
1581
|
"flow_name": "two-factor-auth-app-enrollment",
|
1414
1582
|
}
|
1415
1583
|
payload = {
|
@@ -1463,7 +1631,7 @@ class Client(BaseClient):
|
|
1463
1631
|
"web_modal": 1,
|
1464
1632
|
},
|
1465
1633
|
}
|
1466
|
-
return await self._task(params=
|
1634
|
+
return await self._task(params=query, json=payload)
|
1467
1635
|
|
1468
1636
|
async def _two_factor_enrollment_verify_password_subtask(self, flow_token: str):
|
1469
1637
|
subtask_inputs = [
|
@@ -1561,6 +1729,4 @@ class Client(BaseClient):
|
|
1561
1729
|
if await self.totp_is_enabled():
|
1562
1730
|
return
|
1563
1731
|
|
1564
|
-
# TODO Осторожно, костыль! Перед началом работы вызываем request_user_data, чтоб убедиться что нет других ошибок
|
1565
|
-
await self.request_user_data()
|
1566
1732
|
await self._enable_totp()
|