tweepy-self 1.5.2__tar.gz → 1.6.0__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {tweepy_self-1.5.2 → tweepy_self-1.6.0}/PKG-INFO +1 -1
- {tweepy_self-1.5.2 → tweepy_self-1.6.0}/pyproject.toml +1 -1
- {tweepy_self-1.5.2 → tweepy_self-1.6.0}/twitter/account.py +12 -22
- {tweepy_self-1.5.2 → tweepy_self-1.6.0}/twitter/client.py +118 -25
- tweepy_self-1.6.0/twitter/enums.py +13 -0
- {tweepy_self-1.5.2 → tweepy_self-1.6.0}/twitter/errors.py +5 -0
- {tweepy_self-1.5.2 → tweepy_self-1.6.0}/twitter/models.py +1 -0
- {tweepy_self-1.5.2 → tweepy_self-1.6.0}/README.md +0 -0
- {tweepy_self-1.5.2 → tweepy_self-1.6.0}/twitter/__init__.py +0 -0
- {tweepy_self-1.5.2 → tweepy_self-1.6.0}/twitter/base/__init__.py +0 -0
- {tweepy_self-1.5.2 → tweepy_self-1.6.0}/twitter/base/client.py +0 -0
- {tweepy_self-1.5.2 → tweepy_self-1.6.0}/twitter/base/session.py +0 -0
- {tweepy_self-1.5.2 → tweepy_self-1.6.0}/twitter/utils/__init__.py +0 -0
- {tweepy_self-1.5.2 → tweepy_self-1.6.0}/twitter/utils/file.py +0 -0
- {tweepy_self-1.5.2 → tweepy_self-1.6.0}/twitter/utils/html.py +0 -0
- {tweepy_self-1.5.2 → tweepy_self-1.6.0}/twitter/utils/other.py +0 -0
@@ -1,26 +1,15 @@
|
|
1
1
|
from pathlib import Path
|
2
2
|
from typing import Sequence, Iterable
|
3
|
-
import enum
|
4
3
|
|
5
4
|
from pydantic import BaseModel, Field
|
6
5
|
import pyotp
|
7
6
|
|
8
7
|
from .utils import hidden_value, load_lines, write_lines
|
9
|
-
|
10
|
-
|
11
|
-
class AccountStatus(enum.StrEnum):
|
12
|
-
UNKNOWN = "UNKNOWN"
|
13
|
-
BAD_TOKEN = "BAD_TOKEN"
|
14
|
-
SUSPENDED = "SUSPENDED"
|
15
|
-
LOCKED = "LOCKED"
|
16
|
-
CONSENT_LOCKED = "CONSENT_LOCKED"
|
17
|
-
GOOD = "GOOD"
|
18
|
-
|
19
|
-
def __str__(self):
|
20
|
-
return self.value
|
8
|
+
from .enums import AccountStatus
|
21
9
|
|
22
10
|
|
23
11
|
class Account(BaseModel):
|
12
|
+
# fmt: off
|
24
13
|
auth_token: str | None = Field(default=None, pattern=r"^[a-f0-9]{40}$")
|
25
14
|
ct0: str | None = None
|
26
15
|
id: int | None = None
|
@@ -31,6 +20,7 @@ class Account(BaseModel):
|
|
31
20
|
totp_secret: str | None = None
|
32
21
|
backup_code: str | None = None
|
33
22
|
status: AccountStatus = AccountStatus.UNKNOWN
|
23
|
+
# fmt: on
|
34
24
|
|
35
25
|
@property
|
36
26
|
def hidden_auth_token(self) -> str | None:
|
@@ -62,10 +52,10 @@ class Account(BaseModel):
|
|
62
52
|
|
63
53
|
|
64
54
|
def load_accounts_from_file(
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
55
|
+
filepath: Path | str,
|
56
|
+
*,
|
57
|
+
separator: str = ":",
|
58
|
+
fields: Sequence[str] = ("auth_token", "password", "email", "username"),
|
69
59
|
) -> list[Account]:
|
70
60
|
"""
|
71
61
|
:param filepath: Путь до файла с данными об аккаунтах.
|
@@ -82,11 +72,11 @@ def load_accounts_from_file(
|
|
82
72
|
|
83
73
|
|
84
74
|
def extract_accounts_to_file(
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
75
|
+
filepath: Path | str,
|
76
|
+
accounts: Iterable[Account],
|
77
|
+
*,
|
78
|
+
separator: str = ":",
|
79
|
+
fields: Sequence[str] = ("auth_token", "password", "email", "username"),
|
90
80
|
):
|
91
81
|
lines = []
|
92
82
|
for account in accounts:
|
@@ -11,6 +11,7 @@ from python3_capsolver.fun_captcha import FunCaptcha, FunCaptchaTypeEnm
|
|
11
11
|
|
12
12
|
from .errors import (
|
13
13
|
TwitterException,
|
14
|
+
FailedToFindDuplicatePost,
|
14
15
|
HTTPException,
|
15
16
|
BadRequest,
|
16
17
|
Unauthorized,
|
@@ -24,7 +25,7 @@ from .errors import (
|
|
24
25
|
ConsentLocked,
|
25
26
|
Suspended,
|
26
27
|
)
|
27
|
-
from .utils import to_json
|
28
|
+
from .utils import to_json, tweet_url as create_tweet_url
|
28
29
|
from .base import BaseClient
|
29
30
|
from .account import Account, AccountStatus
|
30
31
|
from .models import UserData, Tweet
|
@@ -383,16 +384,37 @@ class Client(BaseClient):
|
|
383
384
|
await self.request_username()
|
384
385
|
return await self._request_user_data(self.account.username)
|
385
386
|
|
386
|
-
async def upload_image(
|
387
|
+
async def upload_image(
|
388
|
+
self,
|
389
|
+
image: bytes,
|
390
|
+
attempts: int = 3,
|
391
|
+
timeout: float | tuple[float, float] = None,
|
392
|
+
) -> int:
|
387
393
|
"""
|
388
394
|
Upload image as bytes.
|
389
395
|
|
396
|
+
Иногда при первой попытке загрузки изображения возвращает 408,
|
397
|
+
после чего повторная попытка загрузки изображения проходит успешно
|
398
|
+
|
390
399
|
:return: Media ID
|
391
400
|
"""
|
392
401
|
url = "https://upload.twitter.com/1.1/media/upload.json"
|
393
402
|
|
394
403
|
data = {"media_data": base64.b64encode(image)}
|
395
|
-
|
404
|
+
|
405
|
+
for attempt in range(attempts):
|
406
|
+
try:
|
407
|
+
response, response_json = await self.request(
|
408
|
+
"POST", url, data=data, timeout=timeout
|
409
|
+
)
|
410
|
+
media_id = response_json["media_id"]
|
411
|
+
return media_id
|
412
|
+
except HTTPException as exc:
|
413
|
+
if attempt < attempts - 1 and exc.response.status_code == 408:
|
414
|
+
continue
|
415
|
+
else:
|
416
|
+
raise
|
417
|
+
|
396
418
|
media_id = response_json["media_id"]
|
397
419
|
return media_id
|
398
420
|
|
@@ -499,7 +521,7 @@ class Client(BaseClient):
|
|
499
521
|
media_id: int | str = None,
|
500
522
|
tweet_id_to_reply: str | int = None,
|
501
523
|
attachment_url: str = None,
|
502
|
-
) ->
|
524
|
+
) -> Tweet:
|
503
525
|
url, query_id = self._action_to_url("CreateTweet")
|
504
526
|
payload = {
|
505
527
|
"variables": {
|
@@ -543,32 +565,103 @@ class Client(BaseClient):
|
|
543
565
|
)
|
544
566
|
|
545
567
|
response, response_json = await self.request("POST", url, json=payload)
|
546
|
-
|
547
|
-
"
|
548
|
-
|
549
|
-
return
|
568
|
+
tweet = Tweet.from_raw_data(
|
569
|
+
response_json["data"]["create_tweet"]["tweet_results"]["result"]
|
570
|
+
)
|
571
|
+
return tweet
|
550
572
|
|
551
|
-
async def
|
552
|
-
|
553
|
-
:
|
554
|
-
|
555
|
-
|
573
|
+
async def _tweet_or_search_duplicate(
|
574
|
+
self,
|
575
|
+
text: str = None,
|
576
|
+
*,
|
577
|
+
media_id: int | str = None,
|
578
|
+
tweet_id_to_reply: str | int = None,
|
579
|
+
attachment_url: str = None,
|
580
|
+
search_duplicate: bool = True,
|
581
|
+
with_tweet_url: bool = True,
|
582
|
+
) -> Tweet:
|
583
|
+
try:
|
584
|
+
tweet = await self._tweet(
|
585
|
+
text,
|
586
|
+
media_id=media_id,
|
587
|
+
tweet_id_to_reply=tweet_id_to_reply,
|
588
|
+
attachment_url=attachment_url,
|
589
|
+
)
|
590
|
+
except HTTPException as exc:
|
591
|
+
if (
|
592
|
+
search_duplicate
|
593
|
+
and 187 in exc.api_codes # duplicate tweet (Status is a duplicate)
|
594
|
+
):
|
595
|
+
tweets = await self.request_tweets(self.account.id)
|
596
|
+
duplicate_tweet = None
|
597
|
+
for tweet_ in tweets:
|
598
|
+
if tweet_.full_text == text:
|
599
|
+
duplicate_tweet = tweet_
|
600
|
+
|
601
|
+
if not duplicate_tweet:
|
602
|
+
raise FailedToFindDuplicatePost(
|
603
|
+
f"Couldn't find a post duplicate in the next 20 posts"
|
604
|
+
)
|
605
|
+
tweet = duplicate_tweet
|
606
|
+
|
607
|
+
else:
|
608
|
+
raise
|
609
|
+
|
610
|
+
if with_tweet_url:
|
611
|
+
if not self.account.username:
|
612
|
+
await self.request_user_data()
|
613
|
+
tweet.url = create_tweet_url(self.account.username, tweet.id)
|
614
|
+
|
615
|
+
return tweet
|
616
|
+
|
617
|
+
async def tweet(
|
618
|
+
self,
|
619
|
+
text: str,
|
620
|
+
*,
|
621
|
+
media_id: int | str = None,
|
622
|
+
search_duplicate: bool = True,
|
623
|
+
with_tweet_url: bool = True,
|
624
|
+
) -> Tweet:
|
625
|
+
return await self._tweet_or_search_duplicate(
|
626
|
+
text,
|
627
|
+
media_id=media_id,
|
628
|
+
search_duplicate=search_duplicate,
|
629
|
+
with_tweet_url=with_tweet_url,
|
630
|
+
)
|
556
631
|
|
557
632
|
async def reply(
|
558
|
-
self,
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
633
|
+
self,
|
634
|
+
tweet_id: str | int,
|
635
|
+
text: str,
|
636
|
+
*,
|
637
|
+
media_id: int | str = None,
|
638
|
+
search_duplicate: bool = True,
|
639
|
+
with_tweet_url: bool = True,
|
640
|
+
) -> Tweet:
|
641
|
+
return await self._tweet_or_search_duplicate(
|
642
|
+
text,
|
643
|
+
media_id=media_id,
|
644
|
+
tweet_id_to_reply=tweet_id,
|
645
|
+
search_duplicate=search_duplicate,
|
646
|
+
with_tweet_url=with_tweet_url,
|
647
|
+
)
|
564
648
|
|
565
649
|
async def quote(
|
566
|
-
self,
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
650
|
+
self,
|
651
|
+
tweet_url: str,
|
652
|
+
text: str,
|
653
|
+
*,
|
654
|
+
media_id: int | str = None,
|
655
|
+
search_duplicate: bool = True,
|
656
|
+
with_tweet_url: bool = True,
|
657
|
+
) -> Tweet:
|
658
|
+
return await self._tweet_or_search_duplicate(
|
659
|
+
text,
|
660
|
+
media_id=media_id,
|
661
|
+
attachment_url=tweet_url,
|
662
|
+
search_duplicate=search_duplicate,
|
663
|
+
with_tweet_url=with_tweet_url,
|
664
|
+
)
|
572
665
|
|
573
666
|
async def vote(
|
574
667
|
self, tweet_id: int | str, card_id: int | str, choice_number: int
|
@@ -4,6 +4,7 @@ from .account import Account
|
|
4
4
|
|
5
5
|
__all__ = [
|
6
6
|
"TwitterException",
|
7
|
+
"FailedToFindDuplicatePost",
|
7
8
|
"HTTPException",
|
8
9
|
"BadRequest",
|
9
10
|
"Unauthorized",
|
@@ -23,6 +24,10 @@ class TwitterException(Exception):
|
|
23
24
|
pass
|
24
25
|
|
25
26
|
|
27
|
+
class FailedToFindDuplicatePost(TwitterException):
|
28
|
+
pass
|
29
|
+
|
30
|
+
|
26
31
|
def _http_exception_message(
|
27
32
|
response: requests.Response,
|
28
33
|
api_errors: list[dict],
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|