tweepy-self 1.6.3__py3-none-any.whl → 1.10.0b1__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.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()
|