rebecca-api 0.1.3__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.
- rebecca/__init__.py +98 -0
- rebecca/api.py +606 -0
- rebecca/enums.py +7 -0
- rebecca/models.py +492 -0
- rebecca/utils.py +30 -0
- rebecca_api-0.1.3.dist-info/METADATA +10 -0
- rebecca_api-0.1.3.dist-info/RECORD +8 -0
- rebecca_api-0.1.3.dist-info/WHEEL +4 -0
rebecca/__init__.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from .api import RebeccaAPI
|
|
2
|
+
from .models import (
|
|
3
|
+
Admin,
|
|
4
|
+
AdminCreate,
|
|
5
|
+
AdminModify,
|
|
6
|
+
BulkGroup,
|
|
7
|
+
BulkUser,
|
|
8
|
+
BulkUsersProxy,
|
|
9
|
+
CoreCreate,
|
|
10
|
+
CoreResponse,
|
|
11
|
+
CoreResponseList,
|
|
12
|
+
CoreStats,
|
|
13
|
+
CreateUserFromTemplate,
|
|
14
|
+
GroupCreate,
|
|
15
|
+
GroupModify,
|
|
16
|
+
GroupResponse,
|
|
17
|
+
GroupsResponse,
|
|
18
|
+
HostBase,
|
|
19
|
+
HostResponse,
|
|
20
|
+
HTTPValidationError,
|
|
21
|
+
ModifyUserByTemplate,
|
|
22
|
+
NextPlanModel,
|
|
23
|
+
NodeCreate,
|
|
24
|
+
NodeModify,
|
|
25
|
+
NodeResponse,
|
|
26
|
+
NodesUsageResponse,
|
|
27
|
+
NodeUsageResponse,
|
|
28
|
+
ProxyHost,
|
|
29
|
+
ProxyInbound,
|
|
30
|
+
ProxySettings,
|
|
31
|
+
SubscriptionUserResponse,
|
|
32
|
+
SystemStats,
|
|
33
|
+
Token,
|
|
34
|
+
UserCreate,
|
|
35
|
+
UserModify,
|
|
36
|
+
UserResponse,
|
|
37
|
+
UsersResponse,
|
|
38
|
+
UserStatus,
|
|
39
|
+
UserTemplateCreate,
|
|
40
|
+
UserTemplateModify,
|
|
41
|
+
UserTemplateResponse,
|
|
42
|
+
UserUsageResponse,
|
|
43
|
+
UserUsagesResponse,
|
|
44
|
+
ValidationError,
|
|
45
|
+
)
|
|
46
|
+
from .utils import RebeccaTokenCache
|
|
47
|
+
from .enums import RoleEnum
|
|
48
|
+
__all__ = (
|
|
49
|
+
"__version__",
|
|
50
|
+
"RebeccaAPI",
|
|
51
|
+
"RebeccaTokenCache",
|
|
52
|
+
"Admin",
|
|
53
|
+
"AdminCreate",
|
|
54
|
+
"AdminModify",
|
|
55
|
+
"UserCreate",
|
|
56
|
+
"UserModify",
|
|
57
|
+
"UserResponse",
|
|
58
|
+
"UsersResponse",
|
|
59
|
+
"UserStatus",
|
|
60
|
+
"UserTemplateCreate",
|
|
61
|
+
"UserTemplateModify",
|
|
62
|
+
"UserTemplateResponse",
|
|
63
|
+
"UserUsageResponse",
|
|
64
|
+
"UserUsagesResponse",
|
|
65
|
+
"NodeUsageResponse",
|
|
66
|
+
"NodesUsageResponse",
|
|
67
|
+
"NodeCreate",
|
|
68
|
+
"NodeModify",
|
|
69
|
+
"NodeResponse",
|
|
70
|
+
"GroupCreate",
|
|
71
|
+
"GroupModify",
|
|
72
|
+
"GroupResponse",
|
|
73
|
+
"GroupsResponse",
|
|
74
|
+
"BulkGroup",
|
|
75
|
+
"HostBase",
|
|
76
|
+
"HostResponse",
|
|
77
|
+
"CoreCreate",
|
|
78
|
+
"CoreResponse",
|
|
79
|
+
"CoreResponseList",
|
|
80
|
+
"ModifyUserByTemplate",
|
|
81
|
+
"CreateUserFromTemplate",
|
|
82
|
+
"BulkUser",
|
|
83
|
+
"BulkUsersProxy",
|
|
84
|
+
"SystemStats",
|
|
85
|
+
"CoreStats",
|
|
86
|
+
"ProxySettings",
|
|
87
|
+
"ProxyHost",
|
|
88
|
+
"ProxyInbound",
|
|
89
|
+
"Token",
|
|
90
|
+
"HTTPValidationError",
|
|
91
|
+
"ValidationError",
|
|
92
|
+
"SubscriptionUserResponse",
|
|
93
|
+
"NextPlanModel",
|
|
94
|
+
# Enum
|
|
95
|
+
"RoleEnum",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
__version__ = "0.1.3"
|
rebecca/api.py
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
import paramiko
|
|
6
|
+
from paramiko.ssh_exception import SSHException
|
|
7
|
+
from sshtunnel import SSHTunnelForwarder
|
|
8
|
+
|
|
9
|
+
from .models import * # noqa: F403
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RebeccaAPI:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
base_url: str,
|
|
16
|
+
*,
|
|
17
|
+
timeout: float = 10.0,
|
|
18
|
+
verify: bool = False,
|
|
19
|
+
ssh_username: Optional[str] = None,
|
|
20
|
+
ssh_host: Optional[str] = None,
|
|
21
|
+
ssh_port: Optional[int] = 22,
|
|
22
|
+
ssh_private_key_path: Optional[str] = None,
|
|
23
|
+
ssh_key_passphrase: Optional[str] = None,
|
|
24
|
+
ssh_password: Optional[str] = None,
|
|
25
|
+
local_bind_host: str = "127.0.0.1",
|
|
26
|
+
local_bind_port: int = 8000,
|
|
27
|
+
remote_bind_host: str = "127.0.0.1",
|
|
28
|
+
remote_bind_port: int = 8000,
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Initializes the RebeccaAPI client with optional SSH tunneling for secure remote access.
|
|
32
|
+
|
|
33
|
+
:param base_url: The base URL of the Rebecca API.
|
|
34
|
+
:param timeout: The request timeout in seconds (default: 10.0).
|
|
35
|
+
:param verify: SSL verification flag; set to False to ignore SSL verification (default: False).
|
|
36
|
+
:param ssh_username: SSH username for tunnel authentication.
|
|
37
|
+
:param ssh_host: SSH server address for setting up the tunnel. If None, no SSH tunnel is used.
|
|
38
|
+
:param ssh_port: SSH port for connecting to the server (default: 22).
|
|
39
|
+
:param ssh_private_key_path: Path to the SSH private key file for authentication.
|
|
40
|
+
:param ssh_key_passphrase: Passphrase for the SSH private key, if applicable.
|
|
41
|
+
:param ssh_password: Password for SSH authentication. Use if no private key is provided.
|
|
42
|
+
:param local_bind_host: Local IP address for binding the SSH tunnel (default: '127.0.0.1').
|
|
43
|
+
:param local_bind_port: Local port for SSH tunnel binding (default: 8000).
|
|
44
|
+
:param remote_bind_host: Remote IP address for binding on the SSH server side (default: '127.0.0.1').
|
|
45
|
+
:param remote_bind_port: Remote port for the SSH server binding (default: 8000).
|
|
46
|
+
|
|
47
|
+
:raises ValueError: If SSH tunneling is requested but neither a private key nor password is provided.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
self.base_url = base_url
|
|
51
|
+
self.timeout = timeout
|
|
52
|
+
self.verify = verify
|
|
53
|
+
self.ssh_username = ssh_username
|
|
54
|
+
self.ssh_host = ssh_host
|
|
55
|
+
self.ssh_port = ssh_port
|
|
56
|
+
self.ssh_private_key_path = ssh_private_key_path
|
|
57
|
+
self.ssh_key_passphrase = ssh_key_passphrase
|
|
58
|
+
self.ssh_password = ssh_password
|
|
59
|
+
self.local_bind_host = local_bind_host
|
|
60
|
+
self.local_bind_port = local_bind_port
|
|
61
|
+
self.remote_bind_host = remote_bind_host
|
|
62
|
+
self.remote_bind_port = remote_bind_port
|
|
63
|
+
self.client = None
|
|
64
|
+
self._tunnel = None
|
|
65
|
+
self._forwarder = None
|
|
66
|
+
if ssh_host and not ssh_private_key_path and not ssh_password:
|
|
67
|
+
raise ValueError("For an SSH tunnel, you must specify either ssh_private_key_path or ssh_password")
|
|
68
|
+
if not ssh_host:
|
|
69
|
+
self.client = httpx.AsyncClient(base_url=self.base_url, verify=self.verify, timeout=self.timeout)
|
|
70
|
+
|
|
71
|
+
def _load_private_key(self, key_path, passphrase):
|
|
72
|
+
key_classes = [paramiko.RSAKey, paramiko.DSSKey, paramiko.ECDSAKey, paramiko.Ed25519Key]
|
|
73
|
+
for key_class in key_classes:
|
|
74
|
+
try:
|
|
75
|
+
if passphrase:
|
|
76
|
+
pkey = key_class.from_private_key_file(key_path, password=passphrase)
|
|
77
|
+
else:
|
|
78
|
+
pkey = key_class.from_private_key_file(key_path)
|
|
79
|
+
return pkey
|
|
80
|
+
except paramiko.ssh_exception.PasswordRequiredException:
|
|
81
|
+
print("Ошибка: Приватный ключ защищен паролем. Пожалуйста, укажите пароль.")
|
|
82
|
+
except SSHException:
|
|
83
|
+
continue
|
|
84
|
+
raise ValueError("Unsupported key format or incorrect passphrase.")
|
|
85
|
+
|
|
86
|
+
def _initialize(self):
|
|
87
|
+
"""Initialization of the SSH tunnel and the HTTP client."""
|
|
88
|
+
if self.ssh_host:
|
|
89
|
+
if self._tunnel and self._tunnel.is_active:
|
|
90
|
+
return
|
|
91
|
+
# Uploading the key using paramiko
|
|
92
|
+
private_key = None
|
|
93
|
+
if self.ssh_private_key_path:
|
|
94
|
+
private_key = self._load_private_key(self.ssh_private_key_path, self.ssh_key_passphrase)
|
|
95
|
+
# Installing an SSH tunnel using sshtunnel
|
|
96
|
+
self._tunnel = SSHTunnelForwarder(
|
|
97
|
+
(self.ssh_host, self.ssh_port),
|
|
98
|
+
ssh_username=self.ssh_username,
|
|
99
|
+
ssh_password=self.ssh_password,
|
|
100
|
+
ssh_pkey=private_key,
|
|
101
|
+
remote_bind_address=(self.remote_bind_host, self.remote_bind_port),
|
|
102
|
+
local_bind_address=(self.local_bind_host, self.local_bind_port),
|
|
103
|
+
)
|
|
104
|
+
self._tunnel.start()
|
|
105
|
+
self.client = httpx.AsyncClient(
|
|
106
|
+
base_url=f"http://{self.local_bind_host}:{self.local_bind_port}",
|
|
107
|
+
timeout=self.timeout,
|
|
108
|
+
verify=self.verify,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# HTTP-client with local URL
|
|
112
|
+
|
|
113
|
+
def _get_headers(self, token: str) -> Dict[str, str]:
|
|
114
|
+
return {"Authorization": f"Bearer {token}"}
|
|
115
|
+
|
|
116
|
+
async def _request(
|
|
117
|
+
self,
|
|
118
|
+
method: str,
|
|
119
|
+
url: str,
|
|
120
|
+
token: Optional[str] = None,
|
|
121
|
+
data: Optional[BaseModel] = None,
|
|
122
|
+
params: Optional[Dict[str, Any]] = None,
|
|
123
|
+
) -> httpx.Response:
|
|
124
|
+
if self.ssh_host and (not self.client or not self._tunnel.is_active):
|
|
125
|
+
# Initialize the HTTP client and SSH tunnel if they are closed
|
|
126
|
+
self._initialize()
|
|
127
|
+
return await self._request(method, url, token, data, params)
|
|
128
|
+
headers = self._get_headers(token) if token else {}
|
|
129
|
+
if data is None:
|
|
130
|
+
json_data = None
|
|
131
|
+
elif hasattr(data, "model_dump"):
|
|
132
|
+
json_data = data.model_dump(exclude_none=True)
|
|
133
|
+
else:
|
|
134
|
+
json_data = data
|
|
135
|
+
params = {k: v for k, v in (params or {}).items() if v is not None}
|
|
136
|
+
response = await self.client.request(method, url, headers=headers, json=json_data, params=params)
|
|
137
|
+
response.raise_for_status()
|
|
138
|
+
return response
|
|
139
|
+
|
|
140
|
+
async def get_token(self, username: str, password: str) -> Token:
|
|
141
|
+
url = "/api/admin/token"
|
|
142
|
+
payload = {
|
|
143
|
+
"grant_type": "password",
|
|
144
|
+
"username": username,
|
|
145
|
+
"password": password,
|
|
146
|
+
"scope": "",
|
|
147
|
+
"client_id": "",
|
|
148
|
+
"client_secret": "",
|
|
149
|
+
}
|
|
150
|
+
if self.ssh_host and (not self.client or not self._tunnel.is_active):
|
|
151
|
+
# Initialize the HTTP client and SSH tunnel if they are closed
|
|
152
|
+
self._initialize()
|
|
153
|
+
return await self.get_token(username, password)
|
|
154
|
+
response = await self.client.post(url, data=payload)
|
|
155
|
+
response.raise_for_status()
|
|
156
|
+
return Token(**response.json())
|
|
157
|
+
|
|
158
|
+
async def get_current_admin(self, token: str) -> Admin:
|
|
159
|
+
url = "/api/admin"
|
|
160
|
+
response = await self._request("GET", url, token)
|
|
161
|
+
return Admin(**response.json())
|
|
162
|
+
|
|
163
|
+
async def create_admin(self, admin: AdminCreate, token: str) -> Admin:
|
|
164
|
+
url = "/api/admin"
|
|
165
|
+
response = await self._request("POST", url, token, data=admin)
|
|
166
|
+
return Admin(**response.json())
|
|
167
|
+
|
|
168
|
+
async def modify_admin(self, username: str, admin: AdminModify, token: str) -> Admin:
|
|
169
|
+
url = f"/api/admin/{username}"
|
|
170
|
+
response = await self._request("PUT", url, token, data=admin)
|
|
171
|
+
return Admin(**response.json())
|
|
172
|
+
|
|
173
|
+
async def remove_admin(self, username: str, token: str) -> None:
|
|
174
|
+
url = f"/api/admin/{username}"
|
|
175
|
+
await self._request("DELETE", url, token)
|
|
176
|
+
|
|
177
|
+
async def get_admins(
|
|
178
|
+
self, token: str, offset: Optional[int] = None, limit: Optional[int] = None, username: Optional[str] = None
|
|
179
|
+
) -> List[Admin]:
|
|
180
|
+
url = "/api/admins"
|
|
181
|
+
params = {"offset": offset, "limit": limit, "username": username}
|
|
182
|
+
response = await self._request("GET", url, token, params=params)
|
|
183
|
+
return [Admin(**admin) for admin in response.json()]
|
|
184
|
+
|
|
185
|
+
async def disable_all_users_admin(self, username: str, token: str) -> None:
|
|
186
|
+
url = f"/api/admin/{username}/users/disable"
|
|
187
|
+
await self._request("POST", url, token)
|
|
188
|
+
|
|
189
|
+
async def activate_all_users_admin(self, username: str, token: str) -> None:
|
|
190
|
+
url = f"/api/admin/{username}/users/activate"
|
|
191
|
+
await self._request("POST", url, token)
|
|
192
|
+
|
|
193
|
+
async def reset_admin_usage(self, username: str, token: str) -> Admin:
|
|
194
|
+
url = f"/api/admin/usage/reset/{username}"
|
|
195
|
+
response = await self._request("POST", url, token)
|
|
196
|
+
return Admin(**response.json())
|
|
197
|
+
|
|
198
|
+
async def get_admin_usage(self, username: str, token: str) -> Admin:
|
|
199
|
+
url = f"/api/admin/usage/{username}"
|
|
200
|
+
response = await self._request("GET", url, token)
|
|
201
|
+
return response.json()
|
|
202
|
+
|
|
203
|
+
async def get_system_stats(self, token: str) -> SystemStats:
|
|
204
|
+
url = "/api/system"
|
|
205
|
+
response = await self._request("GET", url, token)
|
|
206
|
+
return SystemStats(**response.json())
|
|
207
|
+
|
|
208
|
+
async def get_inbounds(self, token: str) -> Dict[str, List[ProxyInbound]]:
|
|
209
|
+
url = "/api/inbounds"
|
|
210
|
+
response = await self._request("GET", url, token)
|
|
211
|
+
return response.json()
|
|
212
|
+
|
|
213
|
+
async def get_hosts(self, token: str) -> Dict[str, List[ProxyHost]]:
|
|
214
|
+
url = "/api/hosts"
|
|
215
|
+
response = await self._request("GET", url, token)
|
|
216
|
+
return response.json()
|
|
217
|
+
|
|
218
|
+
async def modify_hosts(self, hosts: Dict[str, List[ProxyHost]], token: str) -> Dict[str, List[ProxyHost]]:
|
|
219
|
+
url = "/api/hosts"
|
|
220
|
+
|
|
221
|
+
hosts_model = HostsModel(root=hosts)
|
|
222
|
+
response = await self._request("PUT", url, token, data=hosts_model)
|
|
223
|
+
return response.json()
|
|
224
|
+
|
|
225
|
+
async def create_host(self, host: HostBase, token: str) -> HostResponse:
|
|
226
|
+
url = "/api/host"
|
|
227
|
+
response = await self._request("POST", url, token, data=host)
|
|
228
|
+
return HostResponse(**response.json())
|
|
229
|
+
|
|
230
|
+
async def get_host(self, host_id: int, token: str) -> HostResponse:
|
|
231
|
+
url = f"/api/host/{host_id}"
|
|
232
|
+
response = await self._request("GET", url, token)
|
|
233
|
+
return HostResponse(**response.json())
|
|
234
|
+
|
|
235
|
+
async def modify_host(self, host_id: int, host: HostBase, token: str) -> HostResponse:
|
|
236
|
+
url = f"/api/host/{host_id}"
|
|
237
|
+
response = await self._request("PUT", url, token, data=host)
|
|
238
|
+
return HostResponse(**response.json())
|
|
239
|
+
|
|
240
|
+
async def remove_host(self, host_id: int, token: str) -> None:
|
|
241
|
+
url = f"/api/host/{host_id}"
|
|
242
|
+
await self._request("DELETE", url, token)
|
|
243
|
+
|
|
244
|
+
async def get_core_stats(self, token: str) -> CoreStats:
|
|
245
|
+
url = "/api/core"
|
|
246
|
+
response = await self._request("GET", url, token)
|
|
247
|
+
return CoreStats(**response.json())
|
|
248
|
+
|
|
249
|
+
async def create_core_config(self, core: CoreCreate, token: str) -> CoreResponse:
|
|
250
|
+
url = "/api/core"
|
|
251
|
+
response = await self._request("POST", url, token, data=core)
|
|
252
|
+
return CoreResponse(**response.json())
|
|
253
|
+
|
|
254
|
+
async def get_core_config(self, core_id: int, token: str) -> CoreResponse:
|
|
255
|
+
url = f"/api/core/{core_id}"
|
|
256
|
+
response = await self._request("GET", url, token)
|
|
257
|
+
return CoreResponse(**response.json())
|
|
258
|
+
|
|
259
|
+
async def modify_core_config(
|
|
260
|
+
self, core_id: int, config: CoreCreate, token: str, restart_nodes: bool = False
|
|
261
|
+
) -> CoreResponse:
|
|
262
|
+
url = f"/api/core/{core_id}"
|
|
263
|
+
params = {"restart_nodes": restart_nodes}
|
|
264
|
+
response = await self._request("PUT", url, token, data=config, params=params)
|
|
265
|
+
return CoreResponse(**response.json())
|
|
266
|
+
|
|
267
|
+
async def delete_core_config(self, core_id: int, token: str, restart_nodes: bool = False) -> None:
|
|
268
|
+
url = f"/api/core/{core_id}"
|
|
269
|
+
params = {"restart_nodes": restart_nodes}
|
|
270
|
+
await self._request("DELETE", url, token, params=params)
|
|
271
|
+
|
|
272
|
+
async def get_all_cores(
|
|
273
|
+
self, token: str, offset: Optional[int] = None, limit: Optional[int] = None
|
|
274
|
+
) -> CoreResponseList:
|
|
275
|
+
url = "/api/cores"
|
|
276
|
+
params = {"offset": offset, "limit": limit}
|
|
277
|
+
response = await self._request("GET", url, token, params=params)
|
|
278
|
+
return CoreResponseList(**response.json())
|
|
279
|
+
|
|
280
|
+
async def restart_core(self, core_id: int, token: str) -> None:
|
|
281
|
+
url = f"/api/core/{core_id}/restart"
|
|
282
|
+
await self._request("POST", url, token)
|
|
283
|
+
|
|
284
|
+
async def add_user(self, user: UserCreate, token: str) -> UserResponse:
|
|
285
|
+
url = "/api/user"
|
|
286
|
+
response = await self._request("POST", url, token, data=user)
|
|
287
|
+
return UserResponse(**response.json())
|
|
288
|
+
|
|
289
|
+
async def create_user_from_template(self, template_user: CreateUserFromTemplate, token: str) -> UserResponse:
|
|
290
|
+
url = "/api/user/from_template"
|
|
291
|
+
response = await self._request("POST", url, token, data=template_user)
|
|
292
|
+
return UserResponse(**response.json())
|
|
293
|
+
|
|
294
|
+
async def get_user(self, username: str, token: str) -> UserResponse:
|
|
295
|
+
url = f"/api/user/{username}"
|
|
296
|
+
response = await self._request("GET", url, token)
|
|
297
|
+
return UserResponse(**response.json())
|
|
298
|
+
|
|
299
|
+
async def modify_user(self, username: str, user: UserModify, token: str) -> UserResponse:
|
|
300
|
+
url = f"/api/user/{username}"
|
|
301
|
+
response = await self._request("PUT", url, token, data=user)
|
|
302
|
+
return UserResponse(**response.json())
|
|
303
|
+
|
|
304
|
+
async def modify_user_with_template(
|
|
305
|
+
self, username: str, template_user: ModifyUserByTemplate, token: str
|
|
306
|
+
) -> UserResponse:
|
|
307
|
+
url = f"/api/user/from_template/{username}"
|
|
308
|
+
response = await self._request("PUT", url, token, data=template_user)
|
|
309
|
+
return UserResponse(**response.json())
|
|
310
|
+
|
|
311
|
+
async def activate_next_plan(self, username: str, token: str) -> UserResponse:
|
|
312
|
+
url = f"/api/user/{username}/active-next"
|
|
313
|
+
response = await self._request("POST", url, token)
|
|
314
|
+
return UserResponse(**response.json())
|
|
315
|
+
|
|
316
|
+
async def remove_user(self, username: str, token: str) -> None:
|
|
317
|
+
url = f"/api/user/{username}"
|
|
318
|
+
await self._request("DELETE", url, token)
|
|
319
|
+
|
|
320
|
+
async def bulk_modify_users_expire(self, bulk: BulkUser, token: str) -> Dict[str, Any]:
|
|
321
|
+
url = "/api/users/bulk/expire"
|
|
322
|
+
response = await self._request("POST", url, token, data=bulk)
|
|
323
|
+
return response.json()
|
|
324
|
+
|
|
325
|
+
async def bulk_modify_users_datalimit(self, bulk: BulkUser, token: str) -> Dict[str, Any]:
|
|
326
|
+
url = "/api/users/bulk/data_limit"
|
|
327
|
+
response = await self._request("POST", url, token, data=bulk)
|
|
328
|
+
return response.json()
|
|
329
|
+
|
|
330
|
+
async def bulk_modify_users_proxy_settings(self, bulk: BulkUsersProxy, token: str) -> Dict[str, Any]:
|
|
331
|
+
url = "/api/users/bulk/proxy_settings"
|
|
332
|
+
response = await self._request("POST", url, token, data=bulk)
|
|
333
|
+
return response.json()
|
|
334
|
+
|
|
335
|
+
async def reset_user_data_usage(self, username: str, token: str) -> UserResponse:
|
|
336
|
+
url = f"/api/user/{username}/reset"
|
|
337
|
+
response = await self._request("POST", url, token)
|
|
338
|
+
return UserResponse(**response.json())
|
|
339
|
+
|
|
340
|
+
async def revoke_user_subscription(self, username: str, token: str) -> UserResponse:
|
|
341
|
+
url = f"/api/user/{username}/revoke_sub"
|
|
342
|
+
response = await self._request("POST", url, token)
|
|
343
|
+
return UserResponse(**response.json())
|
|
344
|
+
|
|
345
|
+
async def get_users(
|
|
346
|
+
self,
|
|
347
|
+
token: str,
|
|
348
|
+
offset: Optional[int] = None,
|
|
349
|
+
limit: Optional[int] = None,
|
|
350
|
+
username: Optional[List[str]] = None,
|
|
351
|
+
search: Optional[str] = None,
|
|
352
|
+
status: Optional[str] = None,
|
|
353
|
+
sort: Optional[str] = None,
|
|
354
|
+
) -> UsersResponse:
|
|
355
|
+
url = "/api/users"
|
|
356
|
+
params = {
|
|
357
|
+
"offset": offset,
|
|
358
|
+
"limit": limit,
|
|
359
|
+
"username": username,
|
|
360
|
+
"search": search,
|
|
361
|
+
"status": status,
|
|
362
|
+
"sort": sort,
|
|
363
|
+
}
|
|
364
|
+
response = await self._request("GET", url, token, params=params)
|
|
365
|
+
return UsersResponse(**response.json())
|
|
366
|
+
|
|
367
|
+
async def reset_users_data_usage(self, token: str) -> None:
|
|
368
|
+
url = "/api/users/reset"
|
|
369
|
+
await self._request("POST", url, token)
|
|
370
|
+
|
|
371
|
+
async def get_user_data_usage(
|
|
372
|
+
self, username: str, token: str, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None
|
|
373
|
+
) -> UserUsagesResponse:
|
|
374
|
+
if isinstance(start_date, str):
|
|
375
|
+
start_date = datetime.fromisoformat(start_date)
|
|
376
|
+
if isinstance(end_date, str):
|
|
377
|
+
end_date = datetime.fromisoformat(end_date)
|
|
378
|
+
|
|
379
|
+
params = {
|
|
380
|
+
"start": start_date.isoformat(timespec="seconds") if start_date else None,
|
|
381
|
+
"end": end_date.isoformat(timespec="seconds") if end_date else None,
|
|
382
|
+
}
|
|
383
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
384
|
+
url = f"/api/user/{username}/usage"
|
|
385
|
+
response = await self._request("GET", url, token, params=params)
|
|
386
|
+
return UserUsagesResponse(**response.json())
|
|
387
|
+
|
|
388
|
+
async def set_owner(self, username: str, admin_username: str, token: str) -> UserResponse:
|
|
389
|
+
url = f"/api/user/{username}/set-owner?admin_username={admin_username}"
|
|
390
|
+
response = await self._request("PUT", url, token)
|
|
391
|
+
return UserResponse(**response.json())
|
|
392
|
+
|
|
393
|
+
async def get_expired_users(
|
|
394
|
+
self, token: str, expired_before: Optional[str] = None, expired_after: Optional[str] = None
|
|
395
|
+
) -> List[str]:
|
|
396
|
+
url = "/api/users/expired"
|
|
397
|
+
params = {"expired_before": expired_before, "expired_after": expired_after}
|
|
398
|
+
response = await self._request("GET", url, token, params=params)
|
|
399
|
+
return response.json()
|
|
400
|
+
|
|
401
|
+
async def delete_expired_users(
|
|
402
|
+
self, token: str, expired_before: Optional[str] = None, expired_after: Optional[str] = None
|
|
403
|
+
) -> List[str]:
|
|
404
|
+
url = "/api/users/expired"
|
|
405
|
+
params = {"expired_before": expired_before, "expired_after": expired_after}
|
|
406
|
+
response = await self._request("DELETE", url, token, params=params)
|
|
407
|
+
return response.json()
|
|
408
|
+
|
|
409
|
+
async def get_user_templates(
|
|
410
|
+
self, token: str, offset: Optional[int] = None, limit: Optional[int] = None
|
|
411
|
+
) -> List[UserTemplateResponse]:
|
|
412
|
+
url = "/api/user_template"
|
|
413
|
+
params = {"offset": offset, "limit": limit}
|
|
414
|
+
response = await self._request("GET", url, token, params=params)
|
|
415
|
+
return [UserTemplateResponse(**template) for template in response.json()]
|
|
416
|
+
|
|
417
|
+
async def add_user_template(self, template: UserTemplateCreate, token: str) -> UserTemplateResponse:
|
|
418
|
+
url = "/api/user_template"
|
|
419
|
+
response = await self._request("POST", url, token, data=template)
|
|
420
|
+
return UserTemplateResponse(**response.json())
|
|
421
|
+
|
|
422
|
+
async def get_user_template(self, template_id: int, token: str) -> UserTemplateResponse:
|
|
423
|
+
url = f"/api/user_template/{template_id}"
|
|
424
|
+
response = await self._request("GET", url, token)
|
|
425
|
+
return UserTemplateResponse(**response.json())
|
|
426
|
+
|
|
427
|
+
async def modify_user_template(
|
|
428
|
+
self, template_id: int, template: UserTemplateModify, token: str
|
|
429
|
+
) -> UserTemplateResponse:
|
|
430
|
+
url = f"/api/user_template/{template_id}"
|
|
431
|
+
response = await self._request("PUT", url, token, data=template)
|
|
432
|
+
return UserTemplateResponse(**response.json())
|
|
433
|
+
|
|
434
|
+
async def remove_user_template(self, template_id: int, token: str) -> None:
|
|
435
|
+
url = f"/api/user_template/{template_id}"
|
|
436
|
+
await self._request("DELETE", url, token)
|
|
437
|
+
|
|
438
|
+
# Group endpoints
|
|
439
|
+
async def create_group(self, group: GroupCreate, token: str) -> GroupResponse:
|
|
440
|
+
url = "/api/group"
|
|
441
|
+
response = await self._request("POST", url, token, data=group)
|
|
442
|
+
return GroupResponse(**response.json())
|
|
443
|
+
|
|
444
|
+
async def get_group(self, group_id: int, token: str) -> GroupResponse:
|
|
445
|
+
url = f"/api/group/{group_id}"
|
|
446
|
+
response = await self._request("GET", url, token)
|
|
447
|
+
return GroupResponse(**response.json())
|
|
448
|
+
|
|
449
|
+
async def modify_group(self, group_id: int, group: GroupModify, token: str) -> GroupResponse:
|
|
450
|
+
url = f"/api/group/{group_id}"
|
|
451
|
+
response = await self._request("PUT", url, token, data=group)
|
|
452
|
+
return GroupResponse(**response.json())
|
|
453
|
+
|
|
454
|
+
async def remove_group(self, group_id: int, token: str) -> None:
|
|
455
|
+
url = f"/api/group/{group_id}"
|
|
456
|
+
await self._request("DELETE", url, token)
|
|
457
|
+
|
|
458
|
+
async def get_groups(self, token: str, offset: Optional[int] = None, limit: Optional[int] = None) -> GroupsResponse:
|
|
459
|
+
url = "/api/groups"
|
|
460
|
+
params = {"offset": offset, "limit": limit}
|
|
461
|
+
response = await self._request("GET", url, token, params=params)
|
|
462
|
+
return GroupsResponse(**response.json())
|
|
463
|
+
|
|
464
|
+
async def bulk_add_groups_to_users(self, bulk_group: BulkGroup, token: str) -> Dict[str, Any]:
|
|
465
|
+
url = "/api/groups/bulk/add"
|
|
466
|
+
response = await self._request("POST", url, token, data=bulk_group)
|
|
467
|
+
return response.json()
|
|
468
|
+
|
|
469
|
+
async def bulk_remove_users_from_groups(self, bulk_group: BulkGroup, token: str) -> Dict[str, Any]:
|
|
470
|
+
url = "/api/groups/bulk/remove"
|
|
471
|
+
response = await self._request("POST", url, token, data=bulk_group)
|
|
472
|
+
return response.json()
|
|
473
|
+
|
|
474
|
+
async def get_node_settings(self, token: str) -> Dict[str, Any]:
|
|
475
|
+
url = "/api/node/settings"
|
|
476
|
+
response = await self._request("GET", url, token)
|
|
477
|
+
return response.json()
|
|
478
|
+
|
|
479
|
+
async def add_node(self, node: NodeCreate, token: str) -> NodeResponse:
|
|
480
|
+
url = "/api/node"
|
|
481
|
+
response = await self._request("POST", url, token, data=node)
|
|
482
|
+
return NodeResponse(**response.json())
|
|
483
|
+
|
|
484
|
+
async def get_node(self, node_id: int, token: str) -> NodeResponse:
|
|
485
|
+
url = f"/api/node/{node_id}"
|
|
486
|
+
response = await self._request("GET", url, token)
|
|
487
|
+
return NodeResponse(**response.json())
|
|
488
|
+
|
|
489
|
+
async def modify_node(self, node_id: int, node: NodeModify, token: str) -> NodeResponse:
|
|
490
|
+
url = f"/api/node/{node_id}"
|
|
491
|
+
response = await self._request("PUT", url, token, data=node)
|
|
492
|
+
return NodeResponse(**response.json())
|
|
493
|
+
|
|
494
|
+
async def remove_node(self, node_id: int, token: str) -> None:
|
|
495
|
+
url = f"/api/node/{node_id}"
|
|
496
|
+
await self._request("DELETE", url, token)
|
|
497
|
+
|
|
498
|
+
async def reconnect_node(self, node_id: int, token: str) -> None:
|
|
499
|
+
url = f"/api/node/{node_id}/reconnect"
|
|
500
|
+
await self._request("POST", url, token)
|
|
501
|
+
|
|
502
|
+
async def sync_node(self, node_id: int, token: str, flush_users: bool = False) -> Any:
|
|
503
|
+
url = f"/api/node/{node_id}/sync"
|
|
504
|
+
params = {"flush_users": flush_users}
|
|
505
|
+
response = await self._request("PUT", url, token, params=params)
|
|
506
|
+
return response.json()
|
|
507
|
+
|
|
508
|
+
async def node_logs(self, node_id: int, token: str) -> str:
|
|
509
|
+
url = f"/api/node/{node_id}/logs"
|
|
510
|
+
response = await self._request("GET", url, token)
|
|
511
|
+
return response.text
|
|
512
|
+
|
|
513
|
+
async def get_node_stats_periodic(
|
|
514
|
+
self, node_id: int, token: str, start: Optional[str] = None, end: Optional[str] = None, period: str = "hour"
|
|
515
|
+
) -> Any:
|
|
516
|
+
url = f"/api/node/{node_id}/stats"
|
|
517
|
+
params = {"start": start, "end": end, "period": period}
|
|
518
|
+
response = await self._request("GET", url, token, params=params)
|
|
519
|
+
return response.json()
|
|
520
|
+
|
|
521
|
+
async def realtime_node_stats(self, node_id: int, token: str) -> Any:
|
|
522
|
+
url = f"/api/node/{node_id}/realtime_stats"
|
|
523
|
+
response = await self._request("GET", url, token)
|
|
524
|
+
return response.json()
|
|
525
|
+
|
|
526
|
+
async def realtime_nodes_stats(self, token: str) -> Any:
|
|
527
|
+
url = "/api/nodes/realtime_stats"
|
|
528
|
+
response = await self._request("GET", url, token)
|
|
529
|
+
return response.json()
|
|
530
|
+
|
|
531
|
+
async def user_online_stats(self, node_id: int, username: str, token: str) -> Any:
|
|
532
|
+
url = f"/api/node/{node_id}/online_stats/{username}"
|
|
533
|
+
response = await self._request("GET", url, token)
|
|
534
|
+
return response.json()
|
|
535
|
+
|
|
536
|
+
async def user_online_ip_list(self, node_id: int, username: str, token: str) -> Any:
|
|
537
|
+
url = f"/api/node/{node_id}/online_stats/{username}/ip"
|
|
538
|
+
response = await self._request("GET", url, token)
|
|
539
|
+
return response.json()
|
|
540
|
+
|
|
541
|
+
async def clear_usage_data(
|
|
542
|
+
self, table: str, token: str, start: Optional[str] = None, end: Optional[str] = None
|
|
543
|
+
) -> Any:
|
|
544
|
+
url = f"/api/nodes/clear_usage_data/{table}"
|
|
545
|
+
params = {"start": start, "end": end}
|
|
546
|
+
response = await self._request("DELETE", url, token, params=params)
|
|
547
|
+
return response.json()
|
|
548
|
+
|
|
549
|
+
async def get_nodes(self, token: str) -> List[NodeResponse]:
|
|
550
|
+
url = "/api/nodes"
|
|
551
|
+
response = await self._request("GET", url, token)
|
|
552
|
+
return [NodeResponse(**node) for node in response.json()]
|
|
553
|
+
|
|
554
|
+
async def get_usage(self, token: str, start: Optional[str] = None, end: Optional[str] = None) -> NodesUsageResponse:
|
|
555
|
+
url = "/api/nodes/usage"
|
|
556
|
+
params = {"start": start, "end": end}
|
|
557
|
+
response = await self._request("GET", url, token, params=params)
|
|
558
|
+
return NodesUsageResponse(**response.json())
|
|
559
|
+
|
|
560
|
+
async def get_user_subscription_info(self, url: str = None, token: str = None) -> SubscriptionUserResponse:
|
|
561
|
+
if url:
|
|
562
|
+
# Use the provided URL if it is given
|
|
563
|
+
final_url = url + "/info"
|
|
564
|
+
elif token:
|
|
565
|
+
# Form the URL using the token if it is provided
|
|
566
|
+
final_url = f"/sub/{token}/info"
|
|
567
|
+
else:
|
|
568
|
+
raise ValueError("Either url or token must be provided")
|
|
569
|
+
|
|
570
|
+
response = await self._request("GET", final_url)
|
|
571
|
+
return SubscriptionUserResponse(**response.json())
|
|
572
|
+
|
|
573
|
+
async def get_user_usage(
|
|
574
|
+
self, url: str = None, token: str = None, start: Optional[str] = None, end: Optional[str] = None
|
|
575
|
+
) -> Any:
|
|
576
|
+
if url:
|
|
577
|
+
# Use the provided URL if it is given
|
|
578
|
+
final_url = url + "/usage"
|
|
579
|
+
elif token:
|
|
580
|
+
# Form the URL using the token if it is provided
|
|
581
|
+
final_url = f"/sub/{token}/usage"
|
|
582
|
+
else:
|
|
583
|
+
raise ValueError("Either url or token must be provided")
|
|
584
|
+
params = {"start": start, "end": end}
|
|
585
|
+
response = await self._request("GET", final_url, params=params)
|
|
586
|
+
return response.json()
|
|
587
|
+
|
|
588
|
+
async def get_user_subscription_with_client_type(self, client_type: str, url: str = None, token: str = None) -> Any:
|
|
589
|
+
if url:
|
|
590
|
+
# Use the provided URL if it is given
|
|
591
|
+
final_url = url + f"/{client_type}"
|
|
592
|
+
elif token:
|
|
593
|
+
# Form the URL using the token if it is provided
|
|
594
|
+
final_url = f"/sub/{token}/{client_type}"
|
|
595
|
+
else:
|
|
596
|
+
raise ValueError("Either url or token must be provided")
|
|
597
|
+
|
|
598
|
+
response = await self._request("GET", final_url)
|
|
599
|
+
return response.json()
|
|
600
|
+
|
|
601
|
+
async def close(self):
|
|
602
|
+
"""Closing the HTTP client and SSH tunnel."""
|
|
603
|
+
if self.client:
|
|
604
|
+
await self.client.aclose()
|
|
605
|
+
if self._tunnel:
|
|
606
|
+
self._tunnel.stop()
|
rebecca/enums.py
ADDED
rebecca/models.py
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
from typing import Any, ClassVar, Dict, List, Literal, Optional
|
|
2
|
+
from pydantic import BaseModel, RootModel, field_validator, Field
|
|
3
|
+
from .enums import RoleEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Token(BaseModel):
|
|
7
|
+
access_token: str
|
|
8
|
+
token_type: str = "bearer"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Admin(BaseModel):
|
|
12
|
+
id: Optional[int] = None
|
|
13
|
+
username: Optional[str] = None
|
|
14
|
+
role: Optional[RoleEnum] = RoleEnum.standard
|
|
15
|
+
status: Optional[str] = None
|
|
16
|
+
telegram_id: Optional[int] = None
|
|
17
|
+
users_usage: Optional[int] = None
|
|
18
|
+
data_limit: Optional[int] = None
|
|
19
|
+
users_limit: Optional[int] = None
|
|
20
|
+
active_users: Optional[int] = None
|
|
21
|
+
online_users: Optional[int] = None
|
|
22
|
+
limited_users: Optional[int] = None
|
|
23
|
+
expired_users: Optional[int] = None
|
|
24
|
+
|
|
25
|
+
class Config:
|
|
26
|
+
extra = "ignore"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AdminListResponse(BaseModel):
|
|
30
|
+
admins: List[Admin]
|
|
31
|
+
total: int
|
|
32
|
+
|
|
33
|
+
class UsersPermissions(BaseModel):
|
|
34
|
+
create: bool = False
|
|
35
|
+
delete: bool = False
|
|
36
|
+
reset_usage: bool = False
|
|
37
|
+
revoke: bool = False
|
|
38
|
+
create_on_hold: bool = False
|
|
39
|
+
allow_unlimited_data: bool = False
|
|
40
|
+
allow_unlimited_expire: bool = False
|
|
41
|
+
allow_next_plan: bool = False
|
|
42
|
+
max_data_limit_per_user: int = 0
|
|
43
|
+
|
|
44
|
+
class AdminManagementPermissions(BaseModel):
|
|
45
|
+
can_view: bool = False
|
|
46
|
+
can_edit: bool = False
|
|
47
|
+
can_manage_sudo: bool = False
|
|
48
|
+
|
|
49
|
+
class SectionsPermissions(BaseModel):
|
|
50
|
+
usage: bool = False
|
|
51
|
+
admins: bool = False
|
|
52
|
+
services: bool = False
|
|
53
|
+
hosts: bool = False
|
|
54
|
+
nodes: bool = False
|
|
55
|
+
integrations: bool = False
|
|
56
|
+
xray: bool = False
|
|
57
|
+
|
|
58
|
+
class Permissions(BaseModel):
|
|
59
|
+
users: Optional[UsersPermissions] = Field(default_factory=UsersPermissions)
|
|
60
|
+
admin_management: Optional[AdminManagementPermissions] = Field(default_factory=AdminManagementPermissions)
|
|
61
|
+
sections: Optional[SectionsPermissions] = Field(default_factory=SectionsPermissions)
|
|
62
|
+
|
|
63
|
+
class AdminCreate(BaseModel):
|
|
64
|
+
username: str
|
|
65
|
+
password: str
|
|
66
|
+
role: Optional[RoleEnum] = RoleEnum.standard
|
|
67
|
+
permissions: Optional[Permissions] = Field(default_factory=Permissions)
|
|
68
|
+
telegram_id: Optional[int] = None
|
|
69
|
+
status: Optional[str] = "active"
|
|
70
|
+
disabled_reason: Optional[str] = ""
|
|
71
|
+
users_usage: Optional[int] = 0
|
|
72
|
+
data_limit: Optional[int] = 0
|
|
73
|
+
users_limit: Optional[int] = 0
|
|
74
|
+
active_users: Optional[int] = 0
|
|
75
|
+
online_users: Optional[int] = 0
|
|
76
|
+
limited_users: Optional[int] = 0
|
|
77
|
+
expired_users: Optional[int] = 0
|
|
78
|
+
|
|
79
|
+
class Config:
|
|
80
|
+
extra = "ignore"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AdminModify(BaseModel):
|
|
84
|
+
role: Optional[RoleEnum] = RoleEnum.standard
|
|
85
|
+
password: Optional[str] = None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class HTTPValidationError(BaseModel):
|
|
89
|
+
detail: Optional[List[Dict[str, Any]]] = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ProxySettings(BaseModel):
|
|
93
|
+
id: Optional[str] = None
|
|
94
|
+
flow: Optional[str] = None
|
|
95
|
+
method: Optional[str] = None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class NextPlanModel(BaseModel):
|
|
99
|
+
add_remaining_traffic: bool = False
|
|
100
|
+
data_limit: Optional[int] = 0
|
|
101
|
+
expire: Optional[int] = 0
|
|
102
|
+
fire_on_either: bool = True
|
|
103
|
+
|
|
104
|
+
@field_validator("data_limit", mode="before")
|
|
105
|
+
def validate_data_limit(cls, value):
|
|
106
|
+
if value is not None and value < 0:
|
|
107
|
+
raise ValueError("Data limit in the next plan must be 0 or greater")
|
|
108
|
+
return value
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class UserCreate(BaseModel):
|
|
112
|
+
username: str
|
|
113
|
+
proxy_settings: Optional[Dict[str, ProxySettings]] = None
|
|
114
|
+
group_ids: Optional[List[int]] = None
|
|
115
|
+
expire: Optional[int] = None
|
|
116
|
+
data_limit: Optional[int] = 0
|
|
117
|
+
data_limit_reset_strategy: Optional[str] = "no_reset"
|
|
118
|
+
note: Optional[str] = None
|
|
119
|
+
sub_updated_at: Optional[str] = None
|
|
120
|
+
sub_last_user_agent: Optional[str] = None
|
|
121
|
+
online_at: Optional[str] = None
|
|
122
|
+
on_hold_expire_duration: Optional[int] = 0
|
|
123
|
+
on_hold_timeout: Optional[str] = None
|
|
124
|
+
status: Literal["active", "on_hold"] = "active"
|
|
125
|
+
next_plan: Optional[NextPlanModel] = None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class UserResponse(BaseModel):
|
|
129
|
+
username: Optional[str] = None
|
|
130
|
+
proxy_settings: Optional[Dict[str, ProxySettings]] = {}
|
|
131
|
+
group_ids: Optional[List[int]] = None
|
|
132
|
+
expire: Optional[int] = None
|
|
133
|
+
data_limit: Optional[int] = None
|
|
134
|
+
data_limit_reset_strategy: Optional[str] = None
|
|
135
|
+
note: Optional[str] = None
|
|
136
|
+
sub_updated_at: Optional[str] = None
|
|
137
|
+
sub_last_user_agent: Optional[str] = None
|
|
138
|
+
online_at: Optional[str] = None
|
|
139
|
+
on_hold_expire_duration: Optional[int] = None
|
|
140
|
+
on_hold_timeout: Optional[str] = None
|
|
141
|
+
status: Literal["active", "disabled", "limited", "expired", "on_hold"] = "active"
|
|
142
|
+
used_traffic: Optional[int] = None
|
|
143
|
+
lifetime_used_traffic: Optional[int] = None
|
|
144
|
+
links: Optional[List[str]] = []
|
|
145
|
+
subscription_url: Optional[str] = None
|
|
146
|
+
subscription_token: Optional[str] = None
|
|
147
|
+
next_plan: Optional[NextPlanModel] = None
|
|
148
|
+
admin: Optional[Admin] = None
|
|
149
|
+
created_at: Optional[str] = None
|
|
150
|
+
|
|
151
|
+
def __init__(self, **data):
|
|
152
|
+
super().__init__(**data)
|
|
153
|
+
if not self.subscription_token and self.subscription_url:
|
|
154
|
+
self.subscription_token = self.subscription_url.split("/")[-1]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class NodeCreate(BaseModel):
|
|
158
|
+
name: str
|
|
159
|
+
address: str
|
|
160
|
+
port: int = 62050
|
|
161
|
+
usage_coefficient: float = 1.0
|
|
162
|
+
connection_type: Optional[str] = None
|
|
163
|
+
server_ca: Optional[str] = None
|
|
164
|
+
keep_alive: Optional[int] = None
|
|
165
|
+
max_logs: Optional[int] = 1000
|
|
166
|
+
core_config_id: Optional[int] = None
|
|
167
|
+
api_key: Optional[str] = None
|
|
168
|
+
gather_logs: Optional[bool] = True
|
|
169
|
+
api_port: Optional[int] = None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class NodeModify(BaseModel):
|
|
173
|
+
name: Optional[str] = None
|
|
174
|
+
address: Optional[str] = None
|
|
175
|
+
port: Optional[int] = None
|
|
176
|
+
api_port: Optional[int] = None
|
|
177
|
+
usage_coefficient: Optional[float] = None
|
|
178
|
+
status: Optional[str] = None
|
|
179
|
+
connection_type: Optional[str] = None
|
|
180
|
+
server_ca: Optional[str] = None
|
|
181
|
+
keep_alive: Optional[int] = None
|
|
182
|
+
max_logs: Optional[int] = None
|
|
183
|
+
core_config_id: Optional[int] = None
|
|
184
|
+
api_key: Optional[str] = None
|
|
185
|
+
gather_logs: Optional[bool] = None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class NodeResponse(BaseModel):
|
|
189
|
+
name: str
|
|
190
|
+
address: str
|
|
191
|
+
port: int
|
|
192
|
+
usage_coefficient: float
|
|
193
|
+
id: int
|
|
194
|
+
api_key: Optional[str] = None
|
|
195
|
+
core_config_id: Optional[int] = None
|
|
196
|
+
xray_version: Optional[str] = None
|
|
197
|
+
node_version: Optional[str] = None
|
|
198
|
+
status: str
|
|
199
|
+
message: Optional[str] = None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class NodeUsageResponse(BaseModel):
|
|
203
|
+
node_id: Optional[int] = None
|
|
204
|
+
node_name: Optional[str] = None
|
|
205
|
+
uplink: Optional[int] = None
|
|
206
|
+
downlink: Optional[int] = None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class NodesUsageResponse(BaseModel):
|
|
210
|
+
usages: List[NodeUsageResponse]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class ProxyHost(BaseModel):
|
|
214
|
+
remark: str
|
|
215
|
+
address: str
|
|
216
|
+
port: Optional[int] = None
|
|
217
|
+
sni: Optional[str] = None
|
|
218
|
+
host: Optional[str] = None
|
|
219
|
+
path: Optional[str] = None
|
|
220
|
+
security: str = "inbound_default"
|
|
221
|
+
alpn: str = ""
|
|
222
|
+
fingerprint: str = ""
|
|
223
|
+
allowinsecure: bool
|
|
224
|
+
is_disabled: bool
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class HostsModel(RootModel):
|
|
228
|
+
root: Dict[str, List[ProxyHost]]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class ProxyInbound(BaseModel):
|
|
232
|
+
tag: str
|
|
233
|
+
protocol: str
|
|
234
|
+
network: str
|
|
235
|
+
tls: str
|
|
236
|
+
port: Any
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class CoreStats(BaseModel):
|
|
240
|
+
version: str
|
|
241
|
+
started: bool
|
|
242
|
+
logs_websocket: str
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class UserModify(BaseModel):
|
|
246
|
+
proxy_settings: Optional[Dict[str, ProxySettings]] = {}
|
|
247
|
+
group_ids: Optional[List[int]] = None
|
|
248
|
+
expire: Optional[int] = None
|
|
249
|
+
data_limit: Optional[int] = None
|
|
250
|
+
data_limit_reset_strategy: Optional[Literal["no_reset", "day", "week", "month", "year"]] = None
|
|
251
|
+
note: Optional[str] = None
|
|
252
|
+
sub_updated_at: Optional[str] = None
|
|
253
|
+
sub_last_user_agent: Optional[str] = None
|
|
254
|
+
online_at: Optional[str] = None
|
|
255
|
+
on_hold_expire_duration: Optional[int] = None
|
|
256
|
+
on_hold_timeout: Optional[str] = None
|
|
257
|
+
status: Optional[Literal["active", "disabled", "limited", "expired", "on_hold"]] = None
|
|
258
|
+
next_plan: Optional[NextPlanModel] = None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class UserTemplateCreate(BaseModel):
|
|
262
|
+
name: Optional[str] = None
|
|
263
|
+
group_ids: Optional[List[int]] = []
|
|
264
|
+
data_limit: int = 0
|
|
265
|
+
expire_duration: int = 0
|
|
266
|
+
extra_settings: Optional[ProxySettings] = None
|
|
267
|
+
status: Literal["active", "on_hold"] = "active"
|
|
268
|
+
reset_usages: Optional[bool] = None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class UserTemplateResponse(BaseModel):
|
|
272
|
+
id: int
|
|
273
|
+
name: Optional[str] = None
|
|
274
|
+
group_ids: Optional[List[int]] = None
|
|
275
|
+
data_limit: int
|
|
276
|
+
expire_duration: int
|
|
277
|
+
extra_settings: Optional[ProxySettings] = None
|
|
278
|
+
status: Literal["active", "on_hold"]
|
|
279
|
+
reset_usages: Optional[bool] = None
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class UserTemplateModify(BaseModel):
|
|
283
|
+
name: Optional[str] = None
|
|
284
|
+
group_ids: Optional[List[int]] = None
|
|
285
|
+
data_limit: Optional[int] = None
|
|
286
|
+
expire_duration: Optional[int] = None
|
|
287
|
+
extra_settings: Optional[ProxySettings] = None
|
|
288
|
+
status: Optional[Literal["active", "on_hold"]] = None
|
|
289
|
+
reset_usages: Optional[bool] = None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class UserUsageResponse(BaseModel):
|
|
293
|
+
node_id: Optional[int]
|
|
294
|
+
node_name: Optional[str]
|
|
295
|
+
used_traffic: Optional[int]
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class UserUsagesResponse(BaseModel):
|
|
299
|
+
username: str
|
|
300
|
+
usages: List[UserUsageResponse]
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class UsersResponse(BaseModel):
|
|
304
|
+
users: List[UserResponse]
|
|
305
|
+
total: int
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class UserStatus(BaseModel):
|
|
309
|
+
enum: ClassVar[List[str]] = ["active", "disabled", "limited", "expired", "on_hold"]
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class ValidationError(BaseModel):
|
|
313
|
+
loc: List[Any]
|
|
314
|
+
msg: str
|
|
315
|
+
type: str
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class SubscriptionUserResponse(BaseModel):
|
|
319
|
+
proxies: Dict[str, Any]
|
|
320
|
+
expire: Optional[int] = None
|
|
321
|
+
data_limit: Optional[int] = None
|
|
322
|
+
data_limit_reset_strategy: str = "no_reset"
|
|
323
|
+
inbounds: Dict[str, List[str]] = {}
|
|
324
|
+
note: Optional[str] = None
|
|
325
|
+
sub_updated_at: Optional[str] = None
|
|
326
|
+
sub_last_user_agent: Optional[str] = None
|
|
327
|
+
online_at: Optional[str] = None
|
|
328
|
+
on_hold_expire_duration: Optional[int] = None
|
|
329
|
+
on_hold_timeout: Optional[str] = None
|
|
330
|
+
auto_delete_in_days: Optional[int] = None
|
|
331
|
+
username: str
|
|
332
|
+
status: str
|
|
333
|
+
used_traffic: int
|
|
334
|
+
lifetime_used_traffic: int = 0
|
|
335
|
+
created_at: str
|
|
336
|
+
links: List[str] = []
|
|
337
|
+
subscription_url: str = ""
|
|
338
|
+
excluded_inbounds: Dict[str, List[str]] = {}
|
|
339
|
+
admin: Optional[Admin] = None
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class SystemStats(BaseModel):
|
|
343
|
+
version: Optional[str] = None
|
|
344
|
+
cpu_cores: Optional[int] = None
|
|
345
|
+
cpu_usage: Optional[float] = None
|
|
346
|
+
total_user: Optional[int] = None
|
|
347
|
+
users_active: Optional[int] = None
|
|
348
|
+
incoming_bandwidth: Optional[int] = None
|
|
349
|
+
outgoing_bandwidth: Optional[int] = None
|
|
350
|
+
incoming_bandwidth_speed: Optional[int] = None
|
|
351
|
+
outgoing_bandwidth_speed: Optional[int] = None
|
|
352
|
+
online_users: Optional[int] = None
|
|
353
|
+
users_on_hold: Optional[int] = None
|
|
354
|
+
users_disabled: Optional[int] = None
|
|
355
|
+
users_expired: Optional[int] = None
|
|
356
|
+
users_limited: Optional[int] = None
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class Settings(BaseModel):
|
|
360
|
+
clients: Optional[List[Dict[str, Any]]] = []
|
|
361
|
+
decryption: Optional[str] = None
|
|
362
|
+
network: Optional[str] = None
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class StreamSettings(BaseModel):
|
|
366
|
+
network: Optional[str] = None
|
|
367
|
+
security: Optional[str] = None
|
|
368
|
+
tcpSettings: Optional[Dict[str, Any]] = {}
|
|
369
|
+
wsSettings: Optional[Dict[str, Any]] = {}
|
|
370
|
+
grpcSettings: Optional[Dict[str, Any]] = {}
|
|
371
|
+
tlsSettings: Optional[Dict[str, Any]] = {}
|
|
372
|
+
realitySettings: Optional[Dict[str, Any]] = {}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class Inbound(BaseModel):
|
|
376
|
+
port: Optional[int] = None
|
|
377
|
+
protocol: Optional[str] = None
|
|
378
|
+
settings: Optional[Settings] = Settings()
|
|
379
|
+
streamSettings: Optional[StreamSettings] = StreamSettings()
|
|
380
|
+
sniffing: Optional[Dict[str, Any]] = {}
|
|
381
|
+
tag: Optional[str] = None
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class Outbound(BaseModel):
|
|
385
|
+
protocol: Optional[str] = None
|
|
386
|
+
settings: Optional[Dict[str, Any]] = {}
|
|
387
|
+
tag: Optional[str] = None
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class RoutingRule(BaseModel):
|
|
391
|
+
type: Optional[str] = None
|
|
392
|
+
ip: Optional[List[str]] = []
|
|
393
|
+
domain: Optional[List[str]] = []
|
|
394
|
+
protocol: Optional[List[str]] = []
|
|
395
|
+
outboundTag: Optional[str] = None
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
class Routing(BaseModel):
|
|
399
|
+
domainStrategy: Optional[str] = None
|
|
400
|
+
rules: Optional[List[RoutingRule]] = []
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class CoreConfig(BaseModel):
|
|
404
|
+
log: Optional[Dict[str, Any]] = {}
|
|
405
|
+
inbounds: Optional[List[Inbound]] = []
|
|
406
|
+
outbounds: Optional[List[Outbound]] = []
|
|
407
|
+
routing: Optional[Routing] = Routing()
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class GroupBase(BaseModel):
|
|
411
|
+
name: str
|
|
412
|
+
inbound_tags: Optional[List[str]] = []
|
|
413
|
+
is_disabled: Optional[bool] = False
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class GroupCreate(GroupBase):
|
|
417
|
+
pass
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
class GroupModify(GroupBase):
|
|
421
|
+
pass
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class GroupResponse(GroupBase):
|
|
425
|
+
id: int
|
|
426
|
+
total_users: Optional[int] = 0
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class GroupsResponse(BaseModel):
|
|
430
|
+
groups: List[GroupResponse]
|
|
431
|
+
total: int
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class BulkGroup(BaseModel):
|
|
435
|
+
group_ids: List[int]
|
|
436
|
+
admins: Optional[List[int]] = None
|
|
437
|
+
users: Optional[List[int]] = None
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
class HostBase(BaseModel):
|
|
441
|
+
remark: str
|
|
442
|
+
address: str
|
|
443
|
+
port: Optional[int] = None
|
|
444
|
+
sni: Optional[str] = None
|
|
445
|
+
inbound_tag: Optional[str] = None
|
|
446
|
+
priority: Optional[int] = None
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
class HostResponse(HostBase):
|
|
450
|
+
id: Optional[int] = None
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
class CoreCreate(BaseModel):
|
|
454
|
+
config: Dict[str, Any]
|
|
455
|
+
name: Optional[str] = None
|
|
456
|
+
exclude_inbound_tags: Optional[str] = None
|
|
457
|
+
fallbacks_inbound_tags: Optional[str] = None
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
class CoreResponse(CoreCreate):
|
|
461
|
+
id: int
|
|
462
|
+
created_at: Optional[str] = None
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class CoreResponseList(BaseModel):
|
|
466
|
+
count: int
|
|
467
|
+
cores: List[CoreResponse]
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
class ModifyUserByTemplate(BaseModel):
|
|
471
|
+
user_template_id: int
|
|
472
|
+
note: Optional[str] = None
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class CreateUserFromTemplate(ModifyUserByTemplate):
|
|
476
|
+
username: str
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
class BulkUser(BaseModel):
|
|
480
|
+
amount: int
|
|
481
|
+
group_ids: Optional[List[int]] = None
|
|
482
|
+
admins: Optional[List[int]] = None
|
|
483
|
+
users: Optional[List[int]] = None
|
|
484
|
+
status: Optional[List[str]] = None
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
class BulkUsersProxy(BaseModel):
|
|
488
|
+
flow: Optional[str] = None
|
|
489
|
+
method: Optional[str] = None
|
|
490
|
+
group_ids: Optional[List[int]] = None
|
|
491
|
+
admins: Optional[List[int]] = None
|
|
492
|
+
users: Optional[List[int]] = None
|
rebecca/utils.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from rebecca import RebeccaAPI
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RebeccaTokenCache:
|
|
9
|
+
def __init__(self, client: RebeccaAPI, username: str, password: str, token_expire_minutes: int = 1440):
|
|
10
|
+
self._client = client
|
|
11
|
+
self._username = username
|
|
12
|
+
self._password = password
|
|
13
|
+
self._token_expire_minutes = token_expire_minutes
|
|
14
|
+
self._token: Optional[str] = None
|
|
15
|
+
self._exp_at: Optional[datetime] = None
|
|
16
|
+
|
|
17
|
+
async def get_token(self):
|
|
18
|
+
if not self._exp_at or self._exp_at < datetime.now():
|
|
19
|
+
logging.info("Get new token")
|
|
20
|
+
self._token = await self.get_new_token()
|
|
21
|
+
self._exp_at = datetime.now() + timedelta(minutes=self._token_expire_minutes - 1)
|
|
22
|
+
return self._token
|
|
23
|
+
|
|
24
|
+
async def get_new_token(self):
|
|
25
|
+
try:
|
|
26
|
+
token = await self._client.get_token(username=self._username, password=self._password)
|
|
27
|
+
return token.access_token
|
|
28
|
+
except Exception as e:
|
|
29
|
+
logging.error(f"{e}", exc_info=True)
|
|
30
|
+
raise e
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rebecca-api
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: datetime>=5.5
|
|
7
|
+
Requires-Dist: httpx>=0.28.1
|
|
8
|
+
Requires-Dist: paramiko>=4.0.0
|
|
9
|
+
Requires-Dist: pydantic>=2.12.0
|
|
10
|
+
Requires-Dist: sshtunnel>=0.4.0
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
rebecca/__init__.py,sha256=av5dwod0p6uiRpdZNu_UGkowAKDv6q4w9lhRf42w03E,1930
|
|
2
|
+
rebecca/api.py,sha256=fUMa9EdBlMArkJ6V_G_pIzHrNsyPXSa4Kg5pZRIKnC0,26824
|
|
3
|
+
rebecca/enums.py,sha256=h2d1Hq-X4GkKEVjxokCY-a5nVJim2T2RSVn8Jv5uTNI,126
|
|
4
|
+
rebecca/models.py,sha256=D5zUNC0hoOUa_j9JSt1rnGTVbypcdCAWP2sDPCQ6xv0,13672
|
|
5
|
+
rebecca/utils.py,sha256=5JQBa8oLVhBQxHDaccwikRymkprmAsf4rAJ1Uwi_xe0,1092
|
|
6
|
+
rebecca_api-0.1.3.dist-info/METADATA,sha256=WphiAB9IUFj4X7AJjJ98p9kHkZd2dD1SJHDK5ul7fSo,267
|
|
7
|
+
rebecca_api-0.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
+
rebecca_api-0.1.3.dist-info/RECORD,,
|