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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tweepy-self
3
- Version: 1.5.2
3
+ Version: 1.6.0
4
4
  Summary: Twitter (selfbot) for Python!
5
5
  Author: Alen
6
6
  Author-email: alen.kimov@gmail.com
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "tweepy-self"
3
- version = "1.5.2"
3
+ version = "1.6.0"
4
4
  description = "Twitter (selfbot) for Python!"
5
5
  authors = ["Alen <alen.kimov@gmail.com>"]
6
6
  readme = "README.md"
@@ -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
- filepath: Path | str,
66
- *,
67
- separator: str = ":",
68
- fields: Sequence[str] = ("auth_token", "password", "email", "username"),
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
- filepath: Path | str,
86
- accounts: Iterable[Account],
87
- *,
88
- separator: str = ":",
89
- fields: Sequence[str] = ("auth_token", "password", "email", "username"),
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(self, image: bytes) -> int:
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
- response, response_json = await self.request("POST", url, data=data)
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
- ) -> int:
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
- tweet_id = response_json["data"]["create_tweet"]["tweet_results"]["result"][
547
- "rest_id"
548
- ]
549
- return tweet_id
568
+ tweet = Tweet.from_raw_data(
569
+ response_json["data"]["create_tweet"]["tweet_results"]["result"]
570
+ )
571
+ return tweet
550
572
 
551
- async def tweet(self, text: str, *, media_id: int | str = None) -> int:
552
- """
553
- :return: Tweet ID
554
- """
555
- return await self._tweet(text, media_id=media_id)
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, tweet_id: str | int, text: str, *, media_id: int | str = None
559
- ) -> int:
560
- """
561
- :return: Tweet ID
562
- """
563
- return await self._tweet(text, media_id=media_id, tweet_id_to_reply=tweet_id)
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, tweet_url: str, text: str, *, media_id: int | str = None
567
- ) -> int:
568
- """
569
- :return: Tweet ID
570
- """
571
- return await self._tweet(text, media_id=media_id, attachment_url=tweet_url)
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
@@ -0,0 +1,13 @@
1
+ import enum
2
+
3
+
4
+ class AccountStatus(enum.StrEnum):
5
+ UNKNOWN = "UNKNOWN"
6
+ BAD_TOKEN = "BAD_TOKEN"
7
+ SUSPENDED = "SUSPENDED"
8
+ LOCKED = "LOCKED"
9
+ CONSENT_LOCKED = "CONSENT_LOCKED"
10
+ GOOD = "GOOD"
11
+
12
+ def __str__(self):
13
+ return self.value
@@ -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],
@@ -47,6 +47,7 @@ class Tweet(BaseModel):
47
47
  retweet_count: int
48
48
  retweeted: bool
49
49
  raw_data: dict
50
+ url: str | None = None
50
51
 
51
52
  def __str__(self):
52
53
  short_text = (
File without changes