tweepy-self 0.1.0__py3-none-any.whl → 1.0.0b2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
twitter/errors.py ADDED
@@ -0,0 +1,145 @@
1
+ from curl_cffi import requests
2
+
3
+ from .account import Account
4
+
5
+ __all__ = [
6
+ "TwitterException",
7
+ "HTTPException",
8
+ "BadRequest",
9
+ "Unauthorized",
10
+ "Forbidden",
11
+ "NotFound",
12
+ "RateLimited",
13
+ "ServerError",
14
+ "BadAccount",
15
+ "BadToken",
16
+ "Locked",
17
+ "Suspended",
18
+ ]
19
+
20
+
21
+ class TwitterException(Exception):
22
+ pass
23
+
24
+
25
+ class BadAccount(TwitterException):
26
+ def __init__(
27
+ self,
28
+ account: Account,
29
+ custom_exception_message: str = None,
30
+ ):
31
+ self.account = account
32
+ exception_message = f"Bad Twitter account."
33
+ super().__init__(custom_exception_message or exception_message)
34
+
35
+
36
+ class BadToken(BadAccount):
37
+ def __init__(self, account: Account):
38
+ exception_message = f"Bad Twitter account's auth_token."
39
+ super().__init__(account, custom_exception_message=exception_message)
40
+
41
+
42
+ class Locked(BadAccount):
43
+ def __init__(self, account: Account):
44
+ exception_message = f"Twitter account is locked. Captcha required to unlock."
45
+ super().__init__(account, custom_exception_message=exception_message)
46
+
47
+
48
+ class Suspended(BadAccount):
49
+ def __init__(self, account: Account):
50
+ exception_message = f"Twitter account is suspended."
51
+ super().__init__(account, custom_exception_message=exception_message)
52
+
53
+
54
+ class HTTPException(TwitterException):
55
+ """Exception raised when an HTTP request fails.
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ response: requests.Response,
61
+ data: dict | str,
62
+ custom_exception_message: str = None,
63
+ ):
64
+ self.response = response
65
+ self.api_errors: list[dict[str, int | str]] = []
66
+ self.api_codes: list[int] = []
67
+ self.api_messages: list[str] = []
68
+
69
+ # Если ответ — строка, то это html
70
+ if isinstance(data, str):
71
+ exception_message = f"{response.status_code}"
72
+ if response.status_code == 429:
73
+ exception_message = (f"{response.status_code} Rate limit exceeded."
74
+ f"\nSet twitter.Client(wait_on_rate_limit=True) to ignore this exception.")
75
+ super().__init__(exception_message)
76
+ return
77
+
78
+ errors = data.get("errors", [])
79
+
80
+ if "error" in data:
81
+ errors.append(data["error"])
82
+ else:
83
+ errors.append(data)
84
+
85
+ error_text = ""
86
+
87
+ for error in errors:
88
+ self.api_errors.append(error)
89
+
90
+ if isinstance(error, str):
91
+ self.api_messages.append(error)
92
+ error_text += '\n' + error
93
+ continue
94
+
95
+ if "code" in error:
96
+ self.api_codes.append(error["code"])
97
+ if "message" in error:
98
+ self.api_messages.append(error["message"])
99
+
100
+ if "code" in error and "message" in error:
101
+ error_text += f"\n{error['code']} - {error['message']}"
102
+ elif "message" in error:
103
+ error_text += '\n' + error["message"]
104
+
105
+ if not error_text and "detail" in data:
106
+ self.api_messages.append(data["detail"])
107
+ error_text = '\n' + data["detail"]
108
+ exception_message = f"{response.status_code} {error_text}"
109
+ super().__init__(custom_exception_message or exception_message)
110
+
111
+
112
+ class BadRequest(HTTPException):
113
+ """Exception raised for a 400 HTTP status code.
114
+ """
115
+ pass
116
+
117
+
118
+ class Unauthorized(HTTPException):
119
+ """Exception raised for a 401 HTTP status code.
120
+ """
121
+ pass
122
+
123
+
124
+ class Forbidden(HTTPException):
125
+ """Exception raised for a 403 HTTP status code.
126
+ """
127
+ pass
128
+
129
+
130
+ class NotFound(HTTPException):
131
+ """Exception raised for a 404 HTTP status code.
132
+ """
133
+ pass
134
+
135
+
136
+ class RateLimited(HTTPException):
137
+ """Exception raised for a 429 HTTP status code.
138
+ """
139
+ pass
140
+
141
+
142
+ class ServerError(HTTPException):
143
+ """Exception raised for a 5xx HTTP status code.
144
+ """
145
+ pass
twitter/models.py ADDED
@@ -0,0 +1,64 @@
1
+ from datetime import datetime
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from .utils import to_datetime
6
+
7
+
8
+ class UserData(BaseModel):
9
+ id: int
10
+ username: str
11
+ name: str
12
+ created_at: datetime
13
+ description: str
14
+ location: str
15
+ followers_count: int
16
+ friends_count: int
17
+ raw_data: dict
18
+
19
+ def __str__(self):
20
+ return f"({self.id}) @{self.username}"
21
+
22
+ @classmethod
23
+ def from_raw_user_data(cls, data: dict):
24
+ legacy = data["legacy"]
25
+ keys = ("name", "description", "location", "followers_count", "friends_count")
26
+ values = {key: legacy[key] for key in keys}
27
+ values.update({
28
+ "id": int(data["rest_id"]),
29
+ "username": legacy["screen_name"],
30
+ "created_at": to_datetime(legacy["created_at"]),
31
+ "raw_data": data,
32
+ })
33
+ return cls(**values)
34
+
35
+
36
+ class Tweet(BaseModel):
37
+ user_id: int
38
+ id: int
39
+ created_at: datetime
40
+ full_text: str
41
+ lang: str
42
+ favorite_count: int
43
+ quote_count: int
44
+ reply_count: int
45
+ retweet_count: int
46
+ retweeted: bool
47
+ raw_data: dict
48
+
49
+ def __str__(self):
50
+ short_text = f"{self.full_text[:32]}..." if len(self.full_text) > 16 else self.full_text
51
+ return f"({self.id}) {short_text}"
52
+
53
+ @classmethod
54
+ def from_raw_data(cls, data: dict):
55
+ legacy = data['legacy']
56
+ keys = ("full_text", "lang", "favorite_count", "quote_count", "reply_count", "retweet_count", "retweeted")
57
+ values = {key: legacy[key] for key in keys}
58
+ values.update({
59
+ "user_id": int(legacy["user_id_str"]),
60
+ "id": int(legacy["id_str"]),
61
+ "created_at": to_datetime(legacy["created_at"]),
62
+ "raw_data": data
63
+ })
64
+ return cls(**values)
@@ -0,0 +1,42 @@
1
+ from .file import (
2
+ copy_file,
3
+ load_lines,
4
+ load_json,
5
+ load_toml,
6
+ write_lines,
7
+ write_json,
8
+ to_json,
9
+ )
10
+ from .accounts import (
11
+ load_accounts_from_file,
12
+ extract_accounts_to_file,
13
+ )
14
+ from .html import (
15
+ parse_unlock_html,
16
+ parse_oauth_html,
17
+ )
18
+ from .other import (
19
+ remove_at_sign,
20
+ tweet_url,
21
+ to_datetime,
22
+ hidden_value,
23
+ )
24
+
25
+
26
+ __all__ = [
27
+ "copy_file",
28
+ "load_lines",
29
+ "load_json",
30
+ "load_toml",
31
+ "write_lines",
32
+ "write_json",
33
+ "to_json",
34
+ "load_accounts_from_file",
35
+ "extract_accounts_to_file",
36
+ "parse_unlock_html",
37
+ "parse_oauth_html",
38
+ "remove_at_sign",
39
+ "tweet_url",
40
+ "to_datetime",
41
+ "hidden_value",
42
+ ]
@@ -0,0 +1,43 @@
1
+ from pathlib import Path
2
+ from typing import Sequence, Iterable
3
+
4
+ from .file import load_lines, write_lines
5
+ from ..account import Account
6
+
7
+
8
+ def load_accounts_from_file(
9
+ filepath: Path | str,
10
+ *,
11
+ separator: str = ":",
12
+ fields: Sequence[str] = ("auth_token", "password", "email", "username"),
13
+ ) -> list[Account]:
14
+ """
15
+ :param filepath: Путь до файла с данными об аккаунтах.
16
+ :param separator: Разделитель между данными в строке.
17
+ :param fields: Кортеж, содержащий имена полей в порядке их появления в строке.
18
+ :return: Список Twitter аккаунтов.
19
+ """
20
+ accounts = []
21
+ for line in load_lines(filepath):
22
+ data = dict(zip(fields, line.split(separator)))
23
+ data.update({key: None for key in data if not data[key]})
24
+ accounts.append(Account(**data))
25
+ return accounts
26
+
27
+
28
+ def extract_accounts_to_file(
29
+ filepath: Path | str,
30
+ accounts: Iterable[Account],
31
+ *,
32
+ separator: str = ":",
33
+ fields: Sequence[str] = ("auth_token", "password", "email", "username"),
34
+ ):
35
+ lines = []
36
+ for account in accounts:
37
+ account_data = []
38
+ for field_name in fields:
39
+ field = getattr(account, field_name)
40
+ field = field if field is not None else ""
41
+ account_data.append(field)
42
+ lines.append(separator.join(account_data))
43
+ write_lines(filepath, lines)
twitter/utils/file.py ADDED
@@ -0,0 +1,41 @@
1
+ import json
2
+ import shutil
3
+ import tomllib
4
+ from pathlib import Path
5
+ from typing import Iterable
6
+
7
+
8
+ def copy_file(source_path: Path | str, destination_path: Path | str):
9
+ destination_path = Path(destination_path)
10
+ if destination_path.exists():
11
+ return
12
+ shutil.copy2(str(source_path), str(destination_path))
13
+
14
+
15
+ def load_toml(filepath: Path | str) -> dict:
16
+ with open(filepath, "rb") as file:
17
+ return tomllib.load(file)
18
+
19
+
20
+ def load_lines(filepath: Path | str) -> list[str]:
21
+ with open(filepath, "r") as file:
22
+ return [line.strip() for line in file.readlines() if line != "\n"]
23
+
24
+
25
+ def write_lines(filepath: Path | str, lines: Iterable[str]):
26
+ with open(filepath, "w") as file:
27
+ file.write("\n".join(lines))
28
+
29
+
30
+ def load_json(filepath: Path | str) -> dict:
31
+ with open(filepath, "r") as file:
32
+ return json.load(file)
33
+
34
+
35
+ def write_json(filepath: Path | str, data):
36
+ with open(filepath, "w") as file:
37
+ json.dump(data, file, indent=4)
38
+
39
+
40
+ def to_json(obj) -> str:
41
+ return json.dumps(obj, separators=(',', ':'), ensure_ascii=True)
twitter/utils/html.py ADDED
@@ -0,0 +1,29 @@
1
+ from bs4 import BeautifulSoup
2
+
3
+
4
+ def parse_oauth_html(html: str) -> tuple[str | None, str | None, str | None]:
5
+ """
6
+ :return: authenticity_token, redirect_url, redirect_after_login_url
7
+ """
8
+ soup = BeautifulSoup(html, "lxml")
9
+ authenticity_token_element = soup.find("input", {"name": "authenticity_token"})
10
+ authenticity_token = authenticity_token_element.get("value") if authenticity_token_element else None
11
+ redirect_url_element = soup.find("a", text="click here to continue")
12
+ redirect_url = redirect_url_element.get("href") if redirect_url_element else None
13
+ redirect_after_login_element = soup.find("input", {"name": "redirect_after_login"})
14
+ redirect_after_login_url = redirect_after_login_element.get("value") if redirect_after_login_element else None
15
+ return authenticity_token, redirect_url, redirect_after_login_url
16
+
17
+
18
+ def parse_unlock_html(html: str) -> tuple[str | None, str | None, bool]:
19
+ """
20
+ :return: authenticity_token, assignment_token, needs_unlock
21
+ """
22
+ soup = BeautifulSoup(html, "lxml")
23
+ authenticity_token_element = soup.find("input", {"name": "authenticity_token"})
24
+ authenticity_token = authenticity_token_element.get("value") if authenticity_token_element else None
25
+ assignment_token_element = soup.find("input", {"name": "assignment_token"})
26
+ assignment_token = assignment_token_element.get("value") if assignment_token_element else None
27
+ verification_string = soup.find('input', id='verification_string')
28
+ needs_unlock = bool(verification_string)
29
+ return authenticity_token, assignment_token, needs_unlock
twitter/utils/other.py ADDED
@@ -0,0 +1,24 @@
1
+ from datetime import datetime
2
+
3
+
4
+ def remove_at_sign(username: str) -> str:
5
+ if username.startswith("@"):
6
+ return username[1:]
7
+ return username
8
+
9
+
10
+ def tweet_url(username: str, tweet_id: int) -> str:
11
+ """
12
+ :return: Tweet URL
13
+ """
14
+ return f"https://x.com/{username}/status/{tweet_id}"
15
+
16
+
17
+ def to_datetime(twitter_datetime: str):
18
+ return datetime.strptime(twitter_datetime, '%a %b %d %H:%M:%S +0000 %Y')
19
+
20
+
21
+ def hidden_value(value: str) -> str:
22
+ start = value[:3]
23
+ end = value[-3:]
24
+ return f"{start}**{end}"
tweepy-self/__init__.py DELETED
File without changes
@@ -1,15 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: tweepy-self
3
- Version: 0.1.0
4
- Summary: Twitter (selfbot) for Python!
5
- Author: Alen
6
- Author-email: alen.kimov@gmail.com
7
- Requires-Python: >=3.11,<4.0
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.11
10
- Classifier: Programming Language :: Python :: 3.12
11
- Description-Content-Type: text/markdown
12
-
13
- # Tweepy-self
14
-
15
- https://github.com/alenkimov/better_automation
@@ -1,4 +0,0 @@
1
- tweepy-self/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- tweepy_self-0.1.0.dist-info/METADATA,sha256=lHOX3W_jeCYq7b1s3xGCi4d__V2v3yc9N2IUtvNzJLk,424
3
- tweepy_self-0.1.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
4
- tweepy_self-0.1.0.dist-info/RECORD,,