tweepy-self 0.1.0__py3-none-any.whl → 1.0.0__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.0.0.dist-info/METADATA +216 -0
- tweepy_self-1.0.0.dist-info/RECORD +15 -0
- twitter/__init__.py +26 -0
- twitter/account.py +98 -0
- twitter/base/__init__.py +7 -0
- twitter/base/client.py +20 -0
- twitter/base/session.py +56 -0
- twitter/client.py +1220 -0
- twitter/errors.py +145 -0
- twitter/models.py +64 -0
- twitter/utils/__init__.py +36 -0
- twitter/utils/file.py +41 -0
- twitter/utils/html.py +29 -0
- twitter/utils/other.py +24 -0
- tweepy-self/__init__.py +0 -0
- tweepy_self-0.1.0.dist-info/METADATA +0 -15
- tweepy_self-0.1.0.dist-info/RECORD +0 -4
- {tweepy_self-0.1.0.dist-info → tweepy_self-1.0.0.dist-info}/WHEEL +0 -0
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,36 @@
|
|
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 .html import (
|
11
|
+
parse_unlock_html,
|
12
|
+
parse_oauth_html,
|
13
|
+
)
|
14
|
+
from .other import (
|
15
|
+
remove_at_sign,
|
16
|
+
tweet_url,
|
17
|
+
to_datetime,
|
18
|
+
hidden_value,
|
19
|
+
)
|
20
|
+
|
21
|
+
|
22
|
+
__all__ = [
|
23
|
+
"copy_file",
|
24
|
+
"load_lines",
|
25
|
+
"load_json",
|
26
|
+
"load_toml",
|
27
|
+
"write_lines",
|
28
|
+
"write_json",
|
29
|
+
"to_json",
|
30
|
+
"parse_unlock_html",
|
31
|
+
"parse_oauth_html",
|
32
|
+
"remove_at_sign",
|
33
|
+
"tweet_url",
|
34
|
+
"to_datetime",
|
35
|
+
"hidden_value",
|
36
|
+
]
|
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,,
|
File without changes
|