jugs 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.
- jugs/__init__.py +43 -0
- jugs/auth.py +172 -0
- jugs/client.py +158 -0
- jugs/constants.py +59 -0
- jugs/endpoints/__init__.py +7 -0
- jugs/endpoints/actions.py +89 -0
- jugs/endpoints/discord.py +63 -0
- jugs/endpoints/sys.py +215 -0
- jugs/exceptions.py +29 -0
- jugs/http.py +179 -0
- jugs/models.py +74 -0
- jugs-1.0.0.dist-info/METADATA +10 -0
- jugs-1.0.0.dist-info/RECORD +14 -0
- jugs-1.0.0.dist-info/WHEEL +4 -0
jugs/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from jugs.client import JugsClient
|
|
4
|
+
from jugs.exceptions import (
|
|
5
|
+
AuthError,
|
|
6
|
+
JugsError,
|
|
7
|
+
NotFoundError,
|
|
8
|
+
ProxyError,
|
|
9
|
+
RateLimitError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
)
|
|
12
|
+
from jugs.models import (
|
|
13
|
+
ActionResult,
|
|
14
|
+
ClickResult,
|
|
15
|
+
Comment,
|
|
16
|
+
CommentsResult,
|
|
17
|
+
DiscordMember,
|
|
18
|
+
DiscordUser,
|
|
19
|
+
PresenceResult,
|
|
20
|
+
StatusResult,
|
|
21
|
+
ViewResult,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = (
|
|
25
|
+
"JugsClient",
|
|
26
|
+
"JugsError",
|
|
27
|
+
"AuthError",
|
|
28
|
+
"RateLimitError",
|
|
29
|
+
"NotFoundError",
|
|
30
|
+
"ValidationError",
|
|
31
|
+
"ProxyError",
|
|
32
|
+
"ViewResult",
|
|
33
|
+
"StatusResult",
|
|
34
|
+
"Comment",
|
|
35
|
+
"CommentsResult",
|
|
36
|
+
"DiscordUser",
|
|
37
|
+
"DiscordMember",
|
|
38
|
+
"PresenceResult",
|
|
39
|
+
"ClickResult",
|
|
40
|
+
"ActionResult",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
__version__ = "1.0.0"
|
jugs/auth.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import orjson
|
|
6
|
+
|
|
7
|
+
from jugs.constants import ANON_KEY, BASE_URL, SUPABASE_URL
|
|
8
|
+
from jugs.exceptions import AuthError
|
|
9
|
+
from jugs.http import HTTPSession
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuthManager:
|
|
13
|
+
__slots__ = ("_http", "_access_token", "_refresh_token", "_user")
|
|
14
|
+
|
|
15
|
+
def __init__(self, http: HTTPSession) -> None:
|
|
16
|
+
self._http = http
|
|
17
|
+
self._access_token: str | None = None
|
|
18
|
+
self._refresh_token: str | None = None
|
|
19
|
+
self._user: dict[str, Any] | None = None
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def access_token(self) -> str | None:
|
|
23
|
+
return self._access_token
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def is_authenticated(self) -> bool:
|
|
27
|
+
return self._access_token is not None
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def user(self) -> dict[str, Any] | None:
|
|
31
|
+
return self._user
|
|
32
|
+
|
|
33
|
+
def _supabase_headers(self) -> dict[str, str]:
|
|
34
|
+
headers = {
|
|
35
|
+
"apikey": ANON_KEY,
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if self._access_token:
|
|
40
|
+
headers["Authorization"] = f"Bearer {self._access_token}"
|
|
41
|
+
|
|
42
|
+
return headers
|
|
43
|
+
|
|
44
|
+
def login_email(self, email: str, password: str) -> dict[str, Any]:
|
|
45
|
+
url = f"{SUPABASE_URL}/auth/v1/token?grant_type=password"
|
|
46
|
+
|
|
47
|
+
payload = {
|
|
48
|
+
"email": email,
|
|
49
|
+
"password": password,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
resp = self._http.post(
|
|
53
|
+
url,
|
|
54
|
+
headers=self._supabase_headers(),
|
|
55
|
+
json_data=payload,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if resp.status_code != 200:
|
|
59
|
+
raise AuthError(f"Login failed: {resp.status_code} {resp.text}")
|
|
60
|
+
|
|
61
|
+
data = orjson.loads(resp.content)
|
|
62
|
+
|
|
63
|
+
self._access_token = data.get("access_token")
|
|
64
|
+
self._refresh_token = data.get("refresh_token")
|
|
65
|
+
self._user = data.get("user")
|
|
66
|
+
|
|
67
|
+
if self._access_token:
|
|
68
|
+
self._http.set_cookie("sb-access-token", self._access_token)
|
|
69
|
+
|
|
70
|
+
return data
|
|
71
|
+
|
|
72
|
+
def signup_email(self, email: str, password: str) -> dict[str, Any]:
|
|
73
|
+
url = f"{SUPABASE_URL}/auth/v1/signup"
|
|
74
|
+
|
|
75
|
+
payload = {
|
|
76
|
+
"email": email,
|
|
77
|
+
"password": password,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
resp = self._http.post(
|
|
81
|
+
url,
|
|
82
|
+
headers=self._supabase_headers(),
|
|
83
|
+
json_data=payload,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if resp.status_code not in (200, 201):
|
|
87
|
+
raise AuthError(f"Signup failed: {resp.status_code} {resp.text}")
|
|
88
|
+
|
|
89
|
+
data = orjson.loads(resp.content)
|
|
90
|
+
|
|
91
|
+
self._access_token = data.get("access_token")
|
|
92
|
+
self._refresh_token = data.get("refresh_token")
|
|
93
|
+
self._user = data.get("user") or (data if data.get("id") else None)
|
|
94
|
+
|
|
95
|
+
return data
|
|
96
|
+
|
|
97
|
+
def refresh(self) -> dict[str, Any]:
|
|
98
|
+
if not self._refresh_token:
|
|
99
|
+
raise AuthError("No refresh token available")
|
|
100
|
+
|
|
101
|
+
url = f"{SUPABASE_URL}/auth/v1/token?grant_type=refresh_token"
|
|
102
|
+
|
|
103
|
+
payload = {
|
|
104
|
+
"refresh_token": self._refresh_token,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
resp = self._http.post(
|
|
108
|
+
url,
|
|
109
|
+
headers=self._supabase_headers(),
|
|
110
|
+
json_data=payload,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
if resp.status_code != 200:
|
|
114
|
+
raise AuthError(f"Refresh failed: {resp.status_code} {resp.text}")
|
|
115
|
+
|
|
116
|
+
data = orjson.loads(resp.content)
|
|
117
|
+
|
|
118
|
+
self._access_token = data.get("access_token")
|
|
119
|
+
self._refresh_token = data.get("refresh_token")
|
|
120
|
+
self._user = data.get("user")
|
|
121
|
+
|
|
122
|
+
return data
|
|
123
|
+
|
|
124
|
+
def logout(self) -> None:
|
|
125
|
+
if not self._access_token:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
url = f"{SUPABASE_URL}/auth/v1/logout"
|
|
129
|
+
|
|
130
|
+
self._http.post(
|
|
131
|
+
url,
|
|
132
|
+
headers=self._supabase_headers(),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
self._access_token = None
|
|
136
|
+
self._refresh_token = None
|
|
137
|
+
self._user = None
|
|
138
|
+
self._http.clear_cookies()
|
|
139
|
+
|
|
140
|
+
def load_session(
|
|
141
|
+
self,
|
|
142
|
+
access_token: str,
|
|
143
|
+
refresh_token: str | None = None,
|
|
144
|
+
) -> None:
|
|
145
|
+
self._access_token = access_token
|
|
146
|
+
self._refresh_token = refresh_token
|
|
147
|
+
self._http.set_cookie("sb-access-token", access_token)
|
|
148
|
+
|
|
149
|
+
def discord_oauth_url(self, redirect_to: str = f"{BASE_URL}/dashboard") -> str:
|
|
150
|
+
return (
|
|
151
|
+
f"{SUPABASE_URL}/auth/v1/authorize"
|
|
152
|
+
f"?provider=discord"
|
|
153
|
+
f"&redirect_to={redirect_to}"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def get_session(self) -> dict[str, Any] | None:
|
|
157
|
+
if not self._access_token:
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
url = f"{SUPABASE_URL}/auth/v1/user"
|
|
161
|
+
|
|
162
|
+
resp = self._http.get(
|
|
163
|
+
url,
|
|
164
|
+
headers=self._supabase_headers(),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if resp.status_code != 200:
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
self._user = orjson.loads(resp.content)
|
|
171
|
+
|
|
172
|
+
return self._user
|
jugs/client.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Sequence
|
|
4
|
+
|
|
5
|
+
from jugs.auth import AuthManager
|
|
6
|
+
from jugs.constants import RESERVED_USERNAMES
|
|
7
|
+
from jugs.endpoints.actions import ActionsAPI
|
|
8
|
+
from jugs.endpoints.discord import DiscordAPI
|
|
9
|
+
from jugs.endpoints.sys import SysAPI
|
|
10
|
+
from jugs.exceptions import ValidationError
|
|
11
|
+
from jugs.http import HTTPSession
|
|
12
|
+
from jugs.models import (
|
|
13
|
+
ActionResult,
|
|
14
|
+
ClickResult,
|
|
15
|
+
CommentsResult,
|
|
16
|
+
PresenceResult,
|
|
17
|
+
StatusResult,
|
|
18
|
+
ViewResult,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class JugsClient:
|
|
23
|
+
__slots__ = (
|
|
24
|
+
"_http",
|
|
25
|
+
"_auth",
|
|
26
|
+
"_sys",
|
|
27
|
+
"_discord",
|
|
28
|
+
"_actions",
|
|
29
|
+
"username",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
username: str = "c",
|
|
35
|
+
*,
|
|
36
|
+
proxies: Sequence[str] | None = None,
|
|
37
|
+
timeout: float = 10.0,
|
|
38
|
+
http2: bool = False,
|
|
39
|
+
) -> None:
|
|
40
|
+
if username.lower() in RESERVED_USERNAMES:
|
|
41
|
+
raise ValidationError(f"Username '{username}' is reserved")
|
|
42
|
+
|
|
43
|
+
self.username = username
|
|
44
|
+
self._http = HTTPSession(proxies=proxies, timeout=timeout, http2=http2)
|
|
45
|
+
self._auth = AuthManager(self._http)
|
|
46
|
+
self._sys = SysAPI(self._http)
|
|
47
|
+
self._discord = DiscordAPI(self._http)
|
|
48
|
+
self._actions = ActionsAPI(self._http)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def auth(self) -> AuthManager:
|
|
52
|
+
return self._auth
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def is_authenticated(self) -> bool:
|
|
56
|
+
return self._auth.is_authenticated
|
|
57
|
+
|
|
58
|
+
def view(self, username: str | None = None, *, rotate: bool = False) -> ViewResult:
|
|
59
|
+
return self._sys.view(username or self.username, rotate=rotate)
|
|
60
|
+
|
|
61
|
+
def status(self, username: str | None = None) -> StatusResult:
|
|
62
|
+
return self._sys.status(username or self.username)
|
|
63
|
+
|
|
64
|
+
def likes(self, username: str | None = None) -> StatusResult:
|
|
65
|
+
return self._sys.likes(username or self.username)
|
|
66
|
+
|
|
67
|
+
def click(self, target_url: str, username: str | None = None) -> ClickResult:
|
|
68
|
+
return self._sys.click(username or self.username, target_url)
|
|
69
|
+
|
|
70
|
+
def comments(self, username: str | None = None) -> CommentsResult:
|
|
71
|
+
return self._sys.comments(username or self.username)
|
|
72
|
+
|
|
73
|
+
def post_comment(self, content: str, username: str | None = None) -> dict[str, Any]:
|
|
74
|
+
return self._sys.post_comment(username or self.username, content)
|
|
75
|
+
|
|
76
|
+
def presence(self, discord_id: str) -> PresenceResult:
|
|
77
|
+
return self._discord.presence(discord_id)
|
|
78
|
+
|
|
79
|
+
def audio_proxy(self, audio_url: str) -> bytes:
|
|
80
|
+
return self._sys.audio_proxy(audio_url)
|
|
81
|
+
|
|
82
|
+
def tip(self, payload: list[Any] | None = None) -> ActionResult:
|
|
83
|
+
return self._actions.tip(payload)
|
|
84
|
+
|
|
85
|
+
def claim_drop(self, drop_id: str) -> ActionResult:
|
|
86
|
+
return self._actions.claim_drop(drop_id)
|
|
87
|
+
|
|
88
|
+
def claim_crate(self, crate_id: str) -> ActionResult:
|
|
89
|
+
return self._actions.claim_crate(crate_id)
|
|
90
|
+
|
|
91
|
+
def get_unseen_drops(self) -> ActionResult:
|
|
92
|
+
return self._actions.get_unseen_drops()
|
|
93
|
+
|
|
94
|
+
def get_unseen_crates(self) -> ActionResult:
|
|
95
|
+
return self._actions.get_unseen_crates()
|
|
96
|
+
|
|
97
|
+
def mark_drops_seen(self) -> ActionResult:
|
|
98
|
+
return self._actions.mark_drops_seen()
|
|
99
|
+
|
|
100
|
+
def mark_crates_seen(self) -> ActionResult:
|
|
101
|
+
return self._actions.mark_crates_seen()
|
|
102
|
+
|
|
103
|
+
def bug_check(self) -> dict[str, Any]:
|
|
104
|
+
return self._sys.bug_check()
|
|
105
|
+
|
|
106
|
+
def bug_report(
|
|
107
|
+
self,
|
|
108
|
+
title: str,
|
|
109
|
+
description: str,
|
|
110
|
+
steps: str,
|
|
111
|
+
expected: str,
|
|
112
|
+
actual: str,
|
|
113
|
+
browser: str = "",
|
|
114
|
+
device: str = "",
|
|
115
|
+
) -> dict[str, Any]:
|
|
116
|
+
return self._sys.bug_report(
|
|
117
|
+
title, description, steps, expected, actual, browser, device
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def suggest_check(self) -> dict[str, Any]:
|
|
121
|
+
return self._sys.suggest_check()
|
|
122
|
+
|
|
123
|
+
def suggest(self, suggestion: str, context: str = "") -> dict[str, Any]:
|
|
124
|
+
return self._sys.suggest(suggestion, context)
|
|
125
|
+
|
|
126
|
+
def login(self, email: str, password: str) -> dict[str, Any]:
|
|
127
|
+
return self._auth.login_email(email, password)
|
|
128
|
+
|
|
129
|
+
def signup(self, email: str, password: str) -> dict[str, Any]:
|
|
130
|
+
return self._auth.signup_email(email, password)
|
|
131
|
+
|
|
132
|
+
def logout(self) -> None:
|
|
133
|
+
self._auth.logout()
|
|
134
|
+
|
|
135
|
+
def load_session(
|
|
136
|
+
self,
|
|
137
|
+
access_token: str,
|
|
138
|
+
refresh_token: str | None = None,
|
|
139
|
+
) -> None:
|
|
140
|
+
self._auth.load_session(access_token, refresh_token)
|
|
141
|
+
|
|
142
|
+
def refresh_session(self) -> dict[str, Any]:
|
|
143
|
+
return self._auth.refresh()
|
|
144
|
+
|
|
145
|
+
def discord_oauth_url(self, redirect_to: str | None = None) -> str:
|
|
146
|
+
if redirect_to is None:
|
|
147
|
+
return self._auth.discord_oauth_url()
|
|
148
|
+
|
|
149
|
+
return self._auth.discord_oauth_url(redirect_to)
|
|
150
|
+
|
|
151
|
+
def close(self) -> None:
|
|
152
|
+
self._http.close()
|
|
153
|
+
|
|
154
|
+
def __enter__(self) -> JugsClient:
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
def __exit__(self, *args: object) -> None:
|
|
158
|
+
self.close()
|
jugs/constants.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
BASE_URL: str = "https://www.jugs.lol"
|
|
4
|
+
|
|
5
|
+
SUPABASE_URL: str = "https://ckenyrstsktunenpsubb.supabase.co"
|
|
6
|
+
|
|
7
|
+
ANON_KEY: str = (
|
|
8
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
|
|
9
|
+
"eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNrZW55cnN0c2t0dW5lbnBzdWJiIiwicm9sZSI6ImFub24i"
|
|
10
|
+
"LCJpYXQiOjE3OTE3NzE5ODUsImV4cCI6MjEwNzM0Nzk4NX0."
|
|
11
|
+
"Ggla0o3HGl54t4nj_8gQ67EJDs9UhKlipfhkIrsxg8I"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
DEFAULT_UA: str = (
|
|
15
|
+
"Mozilla/5.0 (Linux; Android 16; moto g play - 2026) "
|
|
16
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
17
|
+
"Chrome/155.0.8059.43 Mobile Safari/537.36"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
ACTION_TIP: str = "70111c9a8fae4bad063a0c4c19787ed3a85c2daa18"
|
|
21
|
+
|
|
22
|
+
ACTION_CLAIM_DROP: str = "40f6997701608f4b9c8cb2f93bf435e26f320c02e9"
|
|
23
|
+
|
|
24
|
+
ACTION_CLAIM_CRATE: str = "403ac738f0b8e509578fdd36336e1ba090ddd1f2d4"
|
|
25
|
+
|
|
26
|
+
ACTION_GET_UNSEEN_DROPS: str = "005deb07fb33ea758655b1173a09d7b399321dfdb5"
|
|
27
|
+
|
|
28
|
+
ACTION_GET_UNSEEN_CRATES: str = "00d777d5d051e684420de77ee2eb388b06c64ad77b"
|
|
29
|
+
|
|
30
|
+
ACTION_MARK_DROPS_SEEN: str = "4035680f86329d45bf40e27a07881bb193653b9610"
|
|
31
|
+
|
|
32
|
+
ACTION_MARK_CRATES_SEEN: str = "409baa837993b269490d97013b1c9529a010fcb913"
|
|
33
|
+
|
|
34
|
+
RESERVED_USERNAMES: frozenset[str] = frozenset(
|
|
35
|
+
{
|
|
36
|
+
"login",
|
|
37
|
+
"signup",
|
|
38
|
+
"dashboard",
|
|
39
|
+
"api",
|
|
40
|
+
"admin",
|
|
41
|
+
"owner",
|
|
42
|
+
"auth",
|
|
43
|
+
"about",
|
|
44
|
+
"terms",
|
|
45
|
+
"privacy",
|
|
46
|
+
"leaderboard",
|
|
47
|
+
"directory",
|
|
48
|
+
"discord",
|
|
49
|
+
"shop",
|
|
50
|
+
"addons",
|
|
51
|
+
"claim",
|
|
52
|
+
"premium",
|
|
53
|
+
"crates",
|
|
54
|
+
"reset-password",
|
|
55
|
+
"account-restricted",
|
|
56
|
+
"_next",
|
|
57
|
+
"static",
|
|
58
|
+
}
|
|
59
|
+
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import orjson
|
|
6
|
+
|
|
7
|
+
from jugs.constants import (
|
|
8
|
+
ACTION_CLAIM_CRATE,
|
|
9
|
+
ACTION_CLAIM_DROP,
|
|
10
|
+
ACTION_GET_UNSEEN_CRATES,
|
|
11
|
+
ACTION_GET_UNSEEN_DROPS,
|
|
12
|
+
ACTION_MARK_CRATES_SEEN,
|
|
13
|
+
ACTION_MARK_DROPS_SEEN,
|
|
14
|
+
ACTION_TIP,
|
|
15
|
+
BASE_URL,
|
|
16
|
+
)
|
|
17
|
+
from jugs.http import HTTPSession
|
|
18
|
+
from jugs.models import ActionResult
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ActionsAPI:
|
|
22
|
+
__slots__ = ("_http",)
|
|
23
|
+
|
|
24
|
+
def __init__(self, http: HTTPSession) -> None:
|
|
25
|
+
self._http = http
|
|
26
|
+
|
|
27
|
+
def _call(
|
|
28
|
+
self,
|
|
29
|
+
action_id: str,
|
|
30
|
+
payload: list[Any] | None = None,
|
|
31
|
+
*,
|
|
32
|
+
path: str = "/c",
|
|
33
|
+
) -> ActionResult:
|
|
34
|
+
if payload is None:
|
|
35
|
+
payload = []
|
|
36
|
+
|
|
37
|
+
url = f"{BASE_URL}{path}"
|
|
38
|
+
|
|
39
|
+
body = orjson.dumps(payload)
|
|
40
|
+
|
|
41
|
+
headers = {
|
|
42
|
+
"Content-Type": "text/plain;charset=UTF-8",
|
|
43
|
+
"Accept": "text/x-component",
|
|
44
|
+
"next-action": action_id,
|
|
45
|
+
"Origin": BASE_URL,
|
|
46
|
+
"Referer": f"{BASE_URL}{path}",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
resp = self._http.post(
|
|
50
|
+
url,
|
|
51
|
+
headers=headers,
|
|
52
|
+
content=body,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
raw = resp.text
|
|
56
|
+
data: Any = None
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
lines = raw.strip().split("\n")
|
|
60
|
+
|
|
61
|
+
for line in lines:
|
|
62
|
+
if line.startswith("1:"):
|
|
63
|
+
data = orjson.loads(line[2:])
|
|
64
|
+
break
|
|
65
|
+
except Exception:
|
|
66
|
+
data = raw
|
|
67
|
+
|
|
68
|
+
return ActionResult(raw=raw, data=data)
|
|
69
|
+
|
|
70
|
+
def tip(self, payload: list[Any] | None = None) -> ActionResult:
|
|
71
|
+
return self._call(ACTION_TIP, payload)
|
|
72
|
+
|
|
73
|
+
def claim_drop(self, drop_id: str) -> ActionResult:
|
|
74
|
+
return self._call(ACTION_CLAIM_DROP, [drop_id])
|
|
75
|
+
|
|
76
|
+
def claim_crate(self, crate_id: str) -> ActionResult:
|
|
77
|
+
return self._call(ACTION_CLAIM_CRATE, [crate_id])
|
|
78
|
+
|
|
79
|
+
def get_unseen_drops(self) -> ActionResult:
|
|
80
|
+
return self._call(ACTION_GET_UNSEEN_DROPS)
|
|
81
|
+
|
|
82
|
+
def get_unseen_crates(self) -> ActionResult:
|
|
83
|
+
return self._call(ACTION_GET_UNSEEN_CRATES)
|
|
84
|
+
|
|
85
|
+
def mark_drops_seen(self) -> ActionResult:
|
|
86
|
+
return self._call(ACTION_MARK_DROPS_SEEN)
|
|
87
|
+
|
|
88
|
+
def mark_crates_seen(self) -> ActionResult:
|
|
89
|
+
return self._call(ACTION_MARK_CRATES_SEEN)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import orjson
|
|
6
|
+
|
|
7
|
+
from jugs.constants import BASE_URL
|
|
8
|
+
from jugs.http import HTTPSession
|
|
9
|
+
from jugs.models import DiscordMember, DiscordUser, PresenceResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DiscordAPI:
|
|
13
|
+
__slots__ = ("_http",)
|
|
14
|
+
|
|
15
|
+
def __init__(self, http: HTTPSession) -> None:
|
|
16
|
+
self._http = http
|
|
17
|
+
|
|
18
|
+
def presence(self, discord_id: str) -> PresenceResult:
|
|
19
|
+
url = f"{BASE_URL}/api/discord/presence/{discord_id}"
|
|
20
|
+
|
|
21
|
+
resp = self._http.get(
|
|
22
|
+
url,
|
|
23
|
+
headers={"Referer": BASE_URL},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
data = orjson.loads(resp.content)
|
|
27
|
+
|
|
28
|
+
user_data = data.get("user")
|
|
29
|
+
member_data = data.get("member")
|
|
30
|
+
|
|
31
|
+
user = None
|
|
32
|
+
|
|
33
|
+
if user_data:
|
|
34
|
+
user = DiscordUser(
|
|
35
|
+
id=str(user_data.get("id") or ""),
|
|
36
|
+
username=str(user_data.get("username") or ""),
|
|
37
|
+
global_name=user_data.get("global_name"),
|
|
38
|
+
avatar_url=user_data.get("avatar_url"),
|
|
39
|
+
avatar_decoration_url=user_data.get("avatar_decoration_url"),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
member = None
|
|
43
|
+
|
|
44
|
+
if member_data:
|
|
45
|
+
roles = tuple(str(r) for r in (member_data.get("roles") or []))
|
|
46
|
+
|
|
47
|
+
member = DiscordMember(
|
|
48
|
+
display_name=member_data.get("display_name"),
|
|
49
|
+
guild_avatar_url=member_data.get("guild_avatar_url"),
|
|
50
|
+
server_booster=bool(member_data.get("server_booster")),
|
|
51
|
+
roles=roles,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
activities = tuple(data.get("activities") or [])
|
|
55
|
+
|
|
56
|
+
return PresenceResult(
|
|
57
|
+
ok=bool(data.get("ok")),
|
|
58
|
+
joined=bool(data.get("joined")),
|
|
59
|
+
status=str(data.get("status") or "offline"),
|
|
60
|
+
activities=activities,
|
|
61
|
+
user=user,
|
|
62
|
+
member=member,
|
|
63
|
+
)
|
jugs/endpoints/sys.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import orjson
|
|
6
|
+
|
|
7
|
+
from jugs.constants import BASE_URL
|
|
8
|
+
from jugs.exceptions import ValidationError
|
|
9
|
+
from jugs.http import HTTPSession
|
|
10
|
+
from jugs.models import (
|
|
11
|
+
ClickResult,
|
|
12
|
+
Comment,
|
|
13
|
+
CommentsResult,
|
|
14
|
+
StatusResult,
|
|
15
|
+
ViewResult,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SysAPI:
|
|
20
|
+
__slots__ = ("_http",)
|
|
21
|
+
|
|
22
|
+
def __init__(self, http: HTTPSession) -> None:
|
|
23
|
+
self._http = http
|
|
24
|
+
|
|
25
|
+
def _headers(self, username: str) -> dict[str, str]:
|
|
26
|
+
return {
|
|
27
|
+
"Origin": BASE_URL,
|
|
28
|
+
"Referer": f"{BASE_URL}/{username}",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def view(self, username: str, *, rotate: bool = False) -> ViewResult:
|
|
32
|
+
url = f"{BASE_URL}/api/sys/v"
|
|
33
|
+
|
|
34
|
+
resp = self._http.post(
|
|
35
|
+
url,
|
|
36
|
+
headers={**self._headers(username), "Content-Type": "application/json"},
|
|
37
|
+
json_data={},
|
|
38
|
+
params={"username": username},
|
|
39
|
+
rotate=rotate,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
data = orjson.loads(resp.content)
|
|
43
|
+
|
|
44
|
+
return ViewResult(
|
|
45
|
+
ok=bool(data.get("ok")),
|
|
46
|
+
counted=bool(data.get("counted")),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def status(self, username: str) -> StatusResult:
|
|
50
|
+
url = f"{BASE_URL}/api/sys/b"
|
|
51
|
+
|
|
52
|
+
resp = self._http.get(
|
|
53
|
+
url,
|
|
54
|
+
headers=self._headers(username),
|
|
55
|
+
params={"username": username},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
data = orjson.loads(resp.content)
|
|
59
|
+
|
|
60
|
+
return StatusResult(
|
|
61
|
+
ok=bool(data.get("ok")),
|
|
62
|
+
bookmarked=bool(data.get("bookmarked")),
|
|
63
|
+
liked=bool(data.get("liked")),
|
|
64
|
+
bookmarks=int(data.get("bookmarks") or 0),
|
|
65
|
+
likes=int(data.get("likes") or 0),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def likes(self, username: str) -> StatusResult:
|
|
69
|
+
url = f"{BASE_URL}/api/sys/l"
|
|
70
|
+
|
|
71
|
+
resp = self._http.get(
|
|
72
|
+
url,
|
|
73
|
+
headers=self._headers(username),
|
|
74
|
+
params={"username": username},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
data = orjson.loads(resp.content)
|
|
78
|
+
|
|
79
|
+
return StatusResult(
|
|
80
|
+
ok=bool(data.get("ok")),
|
|
81
|
+
bookmarked=bool(data.get("bookmarked")),
|
|
82
|
+
liked=bool(data.get("liked")),
|
|
83
|
+
bookmarks=int(data.get("bookmarks") or 0),
|
|
84
|
+
likes=int(data.get("likes") or 0),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def click(self, username: str, target_url: str) -> ClickResult:
|
|
88
|
+
url = f"{BASE_URL}/api/sys/click"
|
|
89
|
+
|
|
90
|
+
payload = orjson.dumps({"username": username, "url": target_url})
|
|
91
|
+
|
|
92
|
+
resp = self._http.post(
|
|
93
|
+
url,
|
|
94
|
+
headers={**self._headers(username), "Content-Type": "text/plain"},
|
|
95
|
+
content=payload,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
data = orjson.loads(resp.content)
|
|
99
|
+
|
|
100
|
+
return ClickResult(ok=bool(data.get("ok")))
|
|
101
|
+
|
|
102
|
+
def comments(self, username: str) -> CommentsResult:
|
|
103
|
+
url = f"{BASE_URL}/api/sys/comments"
|
|
104
|
+
|
|
105
|
+
resp = self._http.get(
|
|
106
|
+
url,
|
|
107
|
+
headers=self._headers(username),
|
|
108
|
+
params={"username": username},
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
data = orjson.loads(resp.content)
|
|
112
|
+
|
|
113
|
+
raw_comments = data.get("comments") or []
|
|
114
|
+
|
|
115
|
+
comments = tuple(
|
|
116
|
+
Comment(
|
|
117
|
+
id=str(c.get("id") or ""),
|
|
118
|
+
author_username=str(c.get("author_username") or ""),
|
|
119
|
+
author_avatar_url=str(c.get("author_avatar_url") or ""),
|
|
120
|
+
content=str(c.get("content") or ""),
|
|
121
|
+
created_at=str(c.get("created_at") or ""),
|
|
122
|
+
)
|
|
123
|
+
for c in raw_comments
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return CommentsResult(
|
|
127
|
+
ok=bool(data.get("ok")),
|
|
128
|
+
comments_enabled=bool(data.get("commentsEnabled")),
|
|
129
|
+
pinned=data.get("pinned"),
|
|
130
|
+
comments=comments,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def post_comment(self, username: str, content: str) -> dict[str, Any]:
|
|
134
|
+
url = f"{BASE_URL}/api/sys/comments"
|
|
135
|
+
|
|
136
|
+
resp = self._http.post(
|
|
137
|
+
url,
|
|
138
|
+
headers={**self._headers(username), "Content-Type": "application/json"},
|
|
139
|
+
json_data={"username": username, "content": content},
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return orjson.loads(resp.content)
|
|
143
|
+
|
|
144
|
+
def audio_proxy(self, audio_url: str) -> bytes:
|
|
145
|
+
url = f"{BASE_URL}/api/sys/audio-proxy"
|
|
146
|
+
|
|
147
|
+
resp = self._http.get(
|
|
148
|
+
url,
|
|
149
|
+
params={"url": audio_url},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if resp.status_code != 200:
|
|
153
|
+
raise ValidationError(f"Audio proxy failed: {resp.status_code}")
|
|
154
|
+
|
|
155
|
+
return resp.content
|
|
156
|
+
|
|
157
|
+
def bug_check(self) -> dict[str, Any]:
|
|
158
|
+
url = f"{BASE_URL}/api/sys/bug"
|
|
159
|
+
|
|
160
|
+
resp = self._http.get(url)
|
|
161
|
+
|
|
162
|
+
return orjson.loads(resp.content)
|
|
163
|
+
|
|
164
|
+
def bug_report(
|
|
165
|
+
self,
|
|
166
|
+
title: str,
|
|
167
|
+
description: str,
|
|
168
|
+
steps: str,
|
|
169
|
+
expected: str,
|
|
170
|
+
actual: str,
|
|
171
|
+
browser: str = "",
|
|
172
|
+
device: str = "",
|
|
173
|
+
) -> dict[str, Any]:
|
|
174
|
+
url = f"{BASE_URL}/api/sys/bug"
|
|
175
|
+
|
|
176
|
+
payload = {
|
|
177
|
+
"title": title,
|
|
178
|
+
"description": description,
|
|
179
|
+
"steps": steps,
|
|
180
|
+
"expected": expected,
|
|
181
|
+
"actual": actual,
|
|
182
|
+
"browser": browser,
|
|
183
|
+
"device": device,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
resp = self._http.post(
|
|
187
|
+
url,
|
|
188
|
+
headers={"Content-Type": "application/json", "Origin": BASE_URL},
|
|
189
|
+
json_data=payload,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return orjson.loads(resp.content)
|
|
193
|
+
|
|
194
|
+
def suggest_check(self) -> dict[str, Any]:
|
|
195
|
+
url = f"{BASE_URL}/api/sys/suggest"
|
|
196
|
+
|
|
197
|
+
resp = self._http.get(url)
|
|
198
|
+
|
|
199
|
+
return orjson.loads(resp.content)
|
|
200
|
+
|
|
201
|
+
def suggest(self, suggestion: str, context: str = "") -> dict[str, Any]:
|
|
202
|
+
url = f"{BASE_URL}/api/sys/suggest"
|
|
203
|
+
|
|
204
|
+
payload = {
|
|
205
|
+
"suggestion": suggestion,
|
|
206
|
+
"context": context,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
resp = self._http.post(
|
|
210
|
+
url,
|
|
211
|
+
headers={"Content-Type": "application/json", "Origin": BASE_URL},
|
|
212
|
+
json_data=payload,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return orjson.loads(resp.content)
|
jugs/exceptions.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class JugsError(Exception):
|
|
5
|
+
__slots__ = ("message",)
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str) -> None:
|
|
8
|
+
self.message = message
|
|
9
|
+
super().__init__(message)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuthError(JugsError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RateLimitError(JugsError):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NotFoundError(JugsError):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ValidationError(JugsError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ProxyError(JugsError):
|
|
29
|
+
pass
|
jugs/http.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
import random
|
|
5
|
+
from typing import Any, Iterator, Mapping, Sequence
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import orjson
|
|
9
|
+
|
|
10
|
+
from jugs.constants import BASE_URL, DEFAULT_UA
|
|
11
|
+
from jugs.exceptions import ProxyError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProxyRotator:
|
|
15
|
+
__slots__ = ("_proxies", "_cycle", "_lock_idx")
|
|
16
|
+
|
|
17
|
+
def __init__(self, proxies: Sequence[str] | None = None) -> None:
|
|
18
|
+
self._proxies: list[str] = list(proxies) if proxies else []
|
|
19
|
+
self._cycle: Iterator[str] | None = None
|
|
20
|
+
self._lock_idx: int = 0
|
|
21
|
+
|
|
22
|
+
if self._proxies:
|
|
23
|
+
self._cycle = itertools.cycle(self._proxies)
|
|
24
|
+
|
|
25
|
+
def next(self) -> str | None:
|
|
26
|
+
if not self._cycle:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
return next(self._cycle)
|
|
30
|
+
|
|
31
|
+
def random(self) -> str | None:
|
|
32
|
+
if not self._proxies:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
return random.choice(self._proxies)
|
|
36
|
+
|
|
37
|
+
def __len__(self) -> int:
|
|
38
|
+
return len(self._proxies)
|
|
39
|
+
|
|
40
|
+
def add(self, proxy: str) -> None:
|
|
41
|
+
self._proxies.append(proxy)
|
|
42
|
+
self._cycle = itertools.cycle(self._proxies)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class HTTPSession:
|
|
46
|
+
__slots__ = (
|
|
47
|
+
"_client",
|
|
48
|
+
"_rotator",
|
|
49
|
+
"_base_headers",
|
|
50
|
+
"_timeout",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
*,
|
|
56
|
+
proxies: Sequence[str] | None = None,
|
|
57
|
+
timeout: float = 10.0,
|
|
58
|
+
http2: bool = False,
|
|
59
|
+
follow_redirects: bool = True,
|
|
60
|
+
) -> None:
|
|
61
|
+
self._rotator = ProxyRotator(proxies)
|
|
62
|
+
self._timeout = timeout
|
|
63
|
+
self._base_headers: dict[str, str] = {
|
|
64
|
+
"User-Agent": DEFAULT_UA,
|
|
65
|
+
"Accept": "*/*",
|
|
66
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
proxy = self._rotator.next()
|
|
70
|
+
|
|
71
|
+
self._client = httpx.Client(
|
|
72
|
+
http2=http2,
|
|
73
|
+
timeout=timeout,
|
|
74
|
+
follow_redirects=follow_redirects,
|
|
75
|
+
proxy=proxy,
|
|
76
|
+
headers=self._base_headers,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def _rotate(self) -> None:
|
|
80
|
+
proxy = self._rotator.next()
|
|
81
|
+
|
|
82
|
+
if proxy is None:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
cookies = self._client.cookies
|
|
86
|
+
self._client.close()
|
|
87
|
+
|
|
88
|
+
self._client = httpx.Client(
|
|
89
|
+
http2=False,
|
|
90
|
+
timeout=self._timeout,
|
|
91
|
+
follow_redirects=True,
|
|
92
|
+
proxy=proxy,
|
|
93
|
+
headers=self._base_headers,
|
|
94
|
+
cookies=cookies,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def request(
|
|
98
|
+
self,
|
|
99
|
+
method: str,
|
|
100
|
+
url: str,
|
|
101
|
+
*,
|
|
102
|
+
headers: Mapping[str, str] | None = None,
|
|
103
|
+
content: bytes | None = None,
|
|
104
|
+
json_data: Any = None,
|
|
105
|
+
params: Mapping[str, str] | None = None,
|
|
106
|
+
rotate: bool = False,
|
|
107
|
+
) -> httpx.Response:
|
|
108
|
+
if rotate and self._rotator:
|
|
109
|
+
self._rotate()
|
|
110
|
+
|
|
111
|
+
req_headers = dict(self._base_headers)
|
|
112
|
+
|
|
113
|
+
if headers:
|
|
114
|
+
req_headers.update(headers)
|
|
115
|
+
|
|
116
|
+
body: bytes | None = content
|
|
117
|
+
|
|
118
|
+
if json_data is not None:
|
|
119
|
+
body = orjson.dumps(json_data)
|
|
120
|
+
req_headers.setdefault("Content-Type", "application/json")
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
return self._client.request(
|
|
124
|
+
method,
|
|
125
|
+
url,
|
|
126
|
+
headers=req_headers,
|
|
127
|
+
content=body,
|
|
128
|
+
params=params,
|
|
129
|
+
)
|
|
130
|
+
except httpx.ProxyError as exc:
|
|
131
|
+
raise ProxyError(str(exc)) from exc
|
|
132
|
+
|
|
133
|
+
def get(
|
|
134
|
+
self,
|
|
135
|
+
url: str,
|
|
136
|
+
*,
|
|
137
|
+
headers: Mapping[str, str] | None = None,
|
|
138
|
+
params: Mapping[str, str] | None = None,
|
|
139
|
+
rotate: bool = False,
|
|
140
|
+
) -> httpx.Response:
|
|
141
|
+
return self.request("GET", url, headers=headers, params=params, rotate=rotate)
|
|
142
|
+
|
|
143
|
+
def post(
|
|
144
|
+
self,
|
|
145
|
+
url: str,
|
|
146
|
+
*,
|
|
147
|
+
headers: Mapping[str, str] | None = None,
|
|
148
|
+
content: bytes | None = None,
|
|
149
|
+
json_data: Any = None,
|
|
150
|
+
params: Mapping[str, str] | None = None,
|
|
151
|
+
rotate: bool = False,
|
|
152
|
+
) -> httpx.Response:
|
|
153
|
+
return self.request(
|
|
154
|
+
"POST",
|
|
155
|
+
url,
|
|
156
|
+
headers=headers,
|
|
157
|
+
content=content,
|
|
158
|
+
json_data=json_data,
|
|
159
|
+
params=params,
|
|
160
|
+
rotate=rotate,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def set_cookie(self, name: str, value: str, domain: str = "www.jugs.lol") -> None:
|
|
164
|
+
self._client.cookies.set(name, value, domain=domain)
|
|
165
|
+
|
|
166
|
+
def get_cookies(self) -> httpx.Cookies:
|
|
167
|
+
return self._client.cookies
|
|
168
|
+
|
|
169
|
+
def clear_cookies(self) -> None:
|
|
170
|
+
self._client.cookies.clear()
|
|
171
|
+
|
|
172
|
+
def close(self) -> None:
|
|
173
|
+
self._client.close()
|
|
174
|
+
|
|
175
|
+
def __enter__(self) -> HTTPSession:
|
|
176
|
+
return self
|
|
177
|
+
|
|
178
|
+
def __exit__(self, *args: object) -> None:
|
|
179
|
+
self.close()
|
jugs/models.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(slots=True, frozen=True)
|
|
8
|
+
class ViewResult:
|
|
9
|
+
ok: bool
|
|
10
|
+
counted: bool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(slots=True, frozen=True)
|
|
14
|
+
class StatusResult:
|
|
15
|
+
ok: bool
|
|
16
|
+
bookmarked: bool
|
|
17
|
+
liked: bool
|
|
18
|
+
bookmarks: int
|
|
19
|
+
likes: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(slots=True, frozen=True)
|
|
23
|
+
class Comment:
|
|
24
|
+
id: str
|
|
25
|
+
author_username: str
|
|
26
|
+
author_avatar_url: str
|
|
27
|
+
content: str
|
|
28
|
+
created_at: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True, frozen=True)
|
|
32
|
+
class CommentsResult:
|
|
33
|
+
ok: bool
|
|
34
|
+
comments_enabled: bool
|
|
35
|
+
pinned: Any
|
|
36
|
+
comments: tuple[Comment, ...]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(slots=True, frozen=True)
|
|
40
|
+
class DiscordUser:
|
|
41
|
+
id: str
|
|
42
|
+
username: str
|
|
43
|
+
global_name: str | None
|
|
44
|
+
avatar_url: str | None
|
|
45
|
+
avatar_decoration_url: str | None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(slots=True, frozen=True)
|
|
49
|
+
class DiscordMember:
|
|
50
|
+
display_name: str | None
|
|
51
|
+
guild_avatar_url: str | None
|
|
52
|
+
server_booster: bool
|
|
53
|
+
roles: tuple[str, ...]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(slots=True, frozen=True)
|
|
57
|
+
class PresenceResult:
|
|
58
|
+
ok: bool
|
|
59
|
+
joined: bool
|
|
60
|
+
status: str
|
|
61
|
+
activities: tuple[dict[str, Any], ...]
|
|
62
|
+
user: DiscordUser | None
|
|
63
|
+
member: DiscordMember | None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(slots=True, frozen=True)
|
|
67
|
+
class ClickResult:
|
|
68
|
+
ok: bool
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(slots=True, frozen=True)
|
|
72
|
+
class ActionResult:
|
|
73
|
+
raw: str
|
|
74
|
+
data: Any
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: jugs
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: High-performance Python client for jugs.lol
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: httpx[http2]>=0.27.0
|
|
7
|
+
Requires-Dist: orjson>=3.10.0
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
10
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
jugs/__init__.py,sha256=vrQQezHzTDJf6DWFNGRtFOllHCEezj-4LCNe3r1S1WI,745
|
|
2
|
+
jugs/auth.py,sha256=q0_2voJlVtCrUkn0lgE6HuTkcZBOWRax_rqGnfzGOuk,4636
|
|
3
|
+
jugs/client.py,sha256=cx6vTisZRHiWj1bpg0lBJn3SglDKFnyCqOu1U_rNwVc,4839
|
|
4
|
+
jugs/constants.py,sha256=2p08gIK6dRvIA1dhygOW3cSjWSzGnj8iEV9m1Vnj_xE,1566
|
|
5
|
+
jugs/exceptions.py,sha256=k5VeincwB2rZivCqdKaqPHTzKYh94PQfIAqQwkBZ0vA,417
|
|
6
|
+
jugs/http.py,sha256=Ins0LoSfwm3j-7FDz2IzLy-GbETUKg79WsqQOUgHxMU,4580
|
|
7
|
+
jugs/models.py,sha256=CE_p9vRBB9WZHRopAbGX-9QSKUfl--DySO3xHNlMvw0,1345
|
|
8
|
+
jugs/endpoints/__init__.py,sha256=REU8O3wbb4X-9OuLZA86tDuJyfXa0J9m5-tvH6G4_Lg,216
|
|
9
|
+
jugs/endpoints/actions.py,sha256=qcl0tqU81YNTJa5caoLMSJoXq7nF5rCPWJIMJyy77dw,2238
|
|
10
|
+
jugs/endpoints/discord.py,sha256=yj94-ZomI0auvntS09s5Tna-CQi2mpJurPFvSzixdGc,1811
|
|
11
|
+
jugs/endpoints/sys.py,sha256=Oek0WVUm_tSnqgwsog3Janw4f4XSAn8iLLAK9XKrrZk,5844
|
|
12
|
+
jugs-1.0.0.dist-info/METADATA,sha256=OHQ74R72k-vcxCKOiWSWvkRYHYlDOPae434qu40NQwI,295
|
|
13
|
+
jugs-1.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
14
|
+
jugs-1.0.0.dist-info/RECORD,,
|