Python-3xui 0.0.2__tar.gz → 0.0.4__tar.gz
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.
- {python_3xui-0.0.2 → python_3xui-0.0.4}/PKG-INFO +1 -1
- {python_3xui-0.0.2 → python_3xui-0.0.4}/pyproject.toml +1 -1
- {python_3xui-0.0.2 → python_3xui-0.0.4}/python_3xui/api.py +28 -8
- {python_3xui-0.0.2 → python_3xui-0.0.4}/.gitignore +0 -0
- {python_3xui-0.0.2 → python_3xui-0.0.4}/LICENSE +0 -0
- {python_3xui-0.0.2 → python_3xui-0.0.4}/README.md +0 -0
- {python_3xui-0.0.2 → python_3xui-0.0.4}/python_3xui/__init__.py +0 -0
- {python_3xui-0.0.2 → python_3xui-0.0.4}/python_3xui/base_model.py +0 -0
- {python_3xui-0.0.2 → python_3xui-0.0.4}/python_3xui/endpoints.py +0 -0
- {python_3xui-0.0.2 → python_3xui-0.0.4}/python_3xui/models.py +0 -0
- {python_3xui-0.0.2 → python_3xui-0.0.4}/python_3xui/util.py +0 -0
- {python_3xui-0.0.2 → python_3xui-0.0.4}/tests/conftest.py +0 -0
- {python_3xui-0.0.2 → python_3xui-0.0.4}/tests/gather_response_stubs.py +0 -0
- {python_3xui-0.0.2 → python_3xui-0.0.4}/tests/pytest.ini +0 -0
- {python_3xui-0.0.2 → python_3xui-0.0.4}/tests/test_endpoints_clients.py +0 -0
- {python_3xui-0.0.2 → python_3xui-0.0.4}/tests/test_endpoints_inbounds.py +0 -0
- {python_3xui-0.0.2 → python_3xui-0.0.4}/tests/test_non_idempotent_endpoints_clients.py +0 -0
- {python_3xui-0.0.2 → python_3xui-0.0.4}/tests/test_non_idempotent_endpoints_inbounds.py +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import time
|
|
1
2
|
from collections.abc import Sequence, Mapping
|
|
2
3
|
from typing import Self, Optional, Dict, Iterable, AsyncIterable, Type, Union, Any, List, Tuple, Literal
|
|
3
4
|
from datetime import datetime, UTC
|
|
4
5
|
|
|
6
|
+
import pyotp
|
|
5
7
|
from httpx import Response, AsyncClient
|
|
6
8
|
from async_lru import alru_cache
|
|
7
9
|
import asyncio
|
|
@@ -73,6 +75,7 @@ class XUIClient:
|
|
|
73
75
|
session_duration: Maximum session duration in seconds. Defaults to 3600.
|
|
74
76
|
"""
|
|
75
77
|
from . import endpoints # look, I know it's bad, but we need to evade cyclical imports
|
|
78
|
+
self.connected: bool = False
|
|
76
79
|
self.PROD_STRING = "tester-777"
|
|
77
80
|
self.session: AsyncClient | None = None
|
|
78
81
|
self.base_host: str = base_website
|
|
@@ -83,13 +86,23 @@ class XUIClient:
|
|
|
83
86
|
self.session_duration: int = session_duration
|
|
84
87
|
self.xui_username: str | None = username
|
|
85
88
|
self.xui_password: str | None = password
|
|
86
|
-
self.
|
|
89
|
+
self.two_fac_secret: str | None = two_fac_code
|
|
90
|
+
self.totp: pyotp.TOTP | None = None
|
|
87
91
|
self.max_retries: int = 5
|
|
88
92
|
self.retry_delay: int = 1
|
|
89
93
|
# endpoints
|
|
90
94
|
self.server_end = endpoints.Server(self)
|
|
91
95
|
self.clients_end = endpoints.Clients(self)
|
|
92
96
|
self.inbounds_end = endpoints.Inbounds(self)
|
|
97
|
+
#init self.totp
|
|
98
|
+
if self.two_fac_secret:
|
|
99
|
+
if self.two_fac_secret.isdigit() and len(self.two_fac_secret) <= 8:
|
|
100
|
+
#print("WARNING: You seem to have entered a 2FA **code**, not a 2FA secret."
|
|
101
|
+
#"Although entering the secret is dangerous, there is no other way to provide a consistent way"
|
|
102
|
+
#"for continuous login. This code will only work for this specific login.")
|
|
103
|
+
self.totp = None
|
|
104
|
+
else:
|
|
105
|
+
self.totp = pyotp.TOTP(self.two_fac_secret)
|
|
93
106
|
|
|
94
107
|
#========================singleton pattern========================
|
|
95
108
|
def __new__(cls, *args, **kwargs):
|
|
@@ -249,6 +262,13 @@ class XUIClient:
|
|
|
249
262
|
"username": self.xui_username,
|
|
250
263
|
"password": self.xui_password,
|
|
251
264
|
}
|
|
265
|
+
if self.totp:
|
|
266
|
+
if self.totp.interval - datetime.now().timestamp() % self.totp.interval < 1:
|
|
267
|
+
await asyncio.sleep(1.1) # just to not submit an invalid code
|
|
268
|
+
payload["twoFactorCode"] = self.totp.now()
|
|
269
|
+
else:
|
|
270
|
+
if self.two_fac_secret:
|
|
271
|
+
payload["twoFactorCode"] = self.two_fac_secret
|
|
252
272
|
|
|
253
273
|
#print(self.session.base_url)
|
|
254
274
|
#print("WE'RE LOGGING IN")
|
|
@@ -272,6 +292,7 @@ class XUIClient:
|
|
|
272
292
|
Self: The XUIClient instance.
|
|
273
293
|
"""
|
|
274
294
|
self.session = AsyncClient(base_url=self.base_url)
|
|
295
|
+
self.connected = True
|
|
275
296
|
return self
|
|
276
297
|
|
|
277
298
|
async def disconnect(self) -> None:
|
|
@@ -279,6 +300,7 @@ class XUIClient:
|
|
|
279
300
|
|
|
280
301
|
This method closes the async HTTP client session.
|
|
281
302
|
"""
|
|
303
|
+
self.connected = False
|
|
282
304
|
await self.session.aclose()
|
|
283
305
|
|
|
284
306
|
async def __aenter__(self) -> Self:
|
|
@@ -313,7 +335,7 @@ class XUIClient:
|
|
|
313
335
|
|
|
314
336
|
#========================inbound management========================
|
|
315
337
|
@alru_cache
|
|
316
|
-
async def get_production_inbounds(self) ->
|
|
338
|
+
async def get_production_inbounds(self) -> Tuple[Inbound, ...]:
|
|
317
339
|
"""Retrieve production inbounds.
|
|
318
340
|
|
|
319
341
|
This method fetches all inbounds and filters them based on the
|
|
@@ -333,7 +355,7 @@ class XUIClient:
|
|
|
333
355
|
if len(usable_inbounds) == 0:
|
|
334
356
|
raise RuntimeError("No production inbounds found! Change prod_string!")
|
|
335
357
|
|
|
336
|
-
return usable_inbounds
|
|
358
|
+
return tuple(usable_inbounds)
|
|
337
359
|
|
|
338
360
|
async def clear_prod_inbound_cache(self):
|
|
339
361
|
"""Clear the production inbound cache.
|
|
@@ -346,15 +368,12 @@ class XUIClient:
|
|
|
346
368
|
This method currently runs every 10 seconds. Please change the
|
|
347
369
|
timer from 5 to 60*60*24 in the code.
|
|
348
370
|
"""
|
|
349
|
-
|
|
350
|
-
# and this will clear the cache every 24 hours or so, to make sure the client
|
|
351
|
-
# always has the most up-to-date inbounds list
|
|
352
|
-
while True:
|
|
371
|
+
while self.connected:
|
|
353
372
|
#print("You're seeing this message because I forgot to remove it in api.update_inbounds() !")
|
|
354
373
|
#print("Please change the timer from 5 to 60*60*24!")
|
|
355
374
|
self.get_production_inbounds.cache_clear()
|
|
356
375
|
await self.get_production_inbounds() #fill the cache
|
|
357
|
-
await asyncio.sleep(10)
|
|
376
|
+
await asyncio.sleep(60*10)
|
|
358
377
|
##print(stat)
|
|
359
378
|
|
|
360
379
|
#========================clients management========================
|
|
@@ -491,6 +510,7 @@ class XUIClient:
|
|
|
491
510
|
email = util.generate_email_from_tgid_inbid(telegram_id, inbound.id)
|
|
492
511
|
resp = await self.clients_end.delete_client_by_email(email, inbound.id)
|
|
493
512
|
responses.append(resp)
|
|
513
|
+
#print("Inbound deleted")
|
|
494
514
|
|
|
495
515
|
return responses
|
|
496
516
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|