Merge pull request #25 from remnawave/development

Bump to 2.3.0
This commit is contained in:
Artem 2025-12-07 22:10:58 +01:00 • committed by GitHub
commit 4db922bf74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1093 additions and 611 deletions

View file

@ -63,7 +63,8 @@ pip install git+https://github.com/remnawave/python-sdk.git@development
| Contract Version | Remnawave Panel Version |
| ---------------- | ----------------------- |
| 2.2.6 | >=2.2.6 |
| 2.3.0 | >=2.3.0 |
| 2.2.6 | ==2.2.6 |
| 2.2.3 | >=2.2.13 |
| 2.1.19 | >=2.1.19, <2.2.0 |
| 2.1.18 | >=2.1.18 |

190
poetry.lock generated
View file

@ -91,6 +91,104 @@ files = [
{file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
]
[[package]]
name = "cffi"
version = "2.0.0"
description = "Foreign Function Interface for Python calling C code."
optional = false
python-versions = ">=3.9"
groups = ["main"]
markers = "platform_python_implementation != \"PyPy\""
files = [
{file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
{file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"},
{file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"},
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"},
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"},
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"},
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"},
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"},
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"},
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"},
{file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"},
{file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"},
{file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"},
{file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"},
{file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"},
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"},
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"},
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"},
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"},
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"},
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"},
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"},
{file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"},
{file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"},
{file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"},
{file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"},
{file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"},
{file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"},
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"},
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"},
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"},
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"},
{file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"},
{file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"},
{file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"},
{file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"},
{file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"},
{file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"},
{file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"},
{file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"},
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"},
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"},
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"},
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"},
{file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"},
{file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"},
{file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"},
{file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"},
{file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"},
{file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"},
{file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"},
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"},
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"},
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"},
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"},
{file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"},
{file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"},
{file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"},
{file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"},
{file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"},
{file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"},
{file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"},
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"},
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"},
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"},
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"},
{file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"},
{file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"},
{file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"},
{file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"},
{file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"},
{file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"},
{file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"},
{file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"},
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"},
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"},
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"},
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"},
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"},
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"},
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"},
{file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"},
{file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"},
{file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"},
]
[package.dependencies]
pycparser = {version = "*", markers = "implementation_name != \"PyPy\""}
[[package]]
name = "click"
version = "8.1.8"
@ -119,6 +217,83 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "cryptography"
version = "46.0.3"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = "!=3.9.0,!=3.9.1,>=3.8"
groups = ["main"]
files = [
{file = "cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a"},
{file = "cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc"},
{file = "cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d"},
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb"},
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849"},
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8"},
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec"},
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91"},
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e"},
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926"},
{file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71"},
{file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac"},
{file = "cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018"},
{file = "cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb"},
{file = "cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c"},
{file = "cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217"},
{file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5"},
{file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715"},
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54"},
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459"},
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422"},
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7"},
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044"},
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665"},
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3"},
{file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20"},
{file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de"},
{file = "cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914"},
{file = "cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db"},
{file = "cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21"},
{file = "cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936"},
{file = "cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683"},
{file = "cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d"},
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0"},
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc"},
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3"},
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971"},
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac"},
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04"},
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506"},
{file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963"},
{file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4"},
{file = "cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df"},
{file = "cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f"},
{file = "cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372"},
{file = "cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32"},
{file = "cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c"},
{file = "cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea"},
{file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b"},
{file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb"},
{file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717"},
{file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9"},
{file = "cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c"},
{file = "cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1"},
]
[package.dependencies]
cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""}
[package.extras]
docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"]
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
nox = ["nox[uv] (>=2024.4.15)"]
pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"]
sdist = ["build (>=1.0.0)"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi (>=2024)", "cryptography-vectors (==46.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
test-randomorder = ["pytest-randomly"]
[[package]]
name = "dnspython"
version = "2.8.0"
@ -401,6 +576,19 @@ files = [
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pycparser"
version = "2.23"
description = "C parser in Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""
files = [
{file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"},
{file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"},
]
[[package]]
name = "pydantic"
version = "2.11.10"
@ -707,4 +895,4 @@ typing-extensions = ">=4.12.0"
[metadata]
lock-version = "2.1"
python-versions = ">=3.11,<4.0"
content-hash = "b3ba7b60ddeb451f296060a09b5ef40ff0cc2ec6464c8520aa3ed40a9bb3ceb0"
content-hash = "2e64664ad5ef0cd863e8a3898493f4cdcf0544ada35d9ecad6aa686b32bdb1ac"

View file

@ -1,7 +1,7 @@
[project]
name = "remnawave"
version = "2.2.6"
description = "A Python SDK for interacting with the Remnawave API v2.2.6."
version = "2.3.0"
description = "A Python SDK for interacting with the Remnawave API v2.3.0."
authors = [
{name = "Artem",email = "dev@forestsnet.com"}
]
@ -15,6 +15,7 @@ dependencies = [
"pydantic[email]>=2.9.2,<3.0.0",
"pydantic-core>=2.33.1,<2.34.0",
"pydantic>=2.9.2,<3.0.0",
"cryptography (>=46.0.3,<47.0.0)",
]
keywords = ["remnawave", "api", "sdk", "proxy", "httpx", "async", "xray"]
classifiers = [

View file

@ -5,58 +5,56 @@ from rapid_api_client.annotations import PydanticBody
from remnawave.models import (
CreateUserRequestDto,
CreateUserResponseDto,
DeleteUserResponseDto,
EmailUserResponseDto,
GetAllUsersResponseDto,
GetAllTagsResponseDto,
GetUserByIdResponseDto,
GetUserByShortUuidResponseDto,
GetUserByUsernameResponseDto,
GetUserByUuidResponseDto,
GetUserAccessibleNodesResponseDto,
GetUserSubscriptionRequestHistoryResponseDto,
TelegramUserResponseDto,
UpdateUserRequestDto,
UserResponseDto,
UsersResponseDto,
TagsResponseDto,
EmailUserResponseDto,
TagUserResponseDto,
UpdateUserRequestDto,
UpdateUserResponseDto,
RevokeUserRequestDto,
GetSubscriptionRequestsResponseDto
)
from remnawave.rapid import BaseController, delete, get, patch, post
class UsersController(BaseController):
@post("/users", response_class=UserResponseDto)
@post("/users", response_class=CreateUserResponseDto)
async def create_user(
self,
body: Annotated[CreateUserRequestDto, PydanticBody()],
) -> UserResponseDto:
"""Create User"""
) -> CreateUserResponseDto:
"""Create a new user"""
...
@patch("/users", response_class=UserResponseDto)
@patch("/users", response_class=UpdateUserResponseDto)
async def update_user(
self,
body: Annotated[UpdateUserRequestDto, PydanticBody()],
) -> UserResponseDto:
"""Update User"""
) -> UpdateUserResponseDto:
"""Update a user by UUID or username"""
...
@get("/users", response_class=UsersResponseDto)
async def get_all_users_v2(
@get("/users", response_class=GetAllUsersResponseDto)
async def get_all_users(
self,
start: Annotated[
int, Query(default=0, ge=0, description="Index to start pagination from")
],
Optional[int],
Query(default=None, description="Offset for pagination")
] = None,
size: Annotated[
int, Query(default=25, ge=1, description="Number of users per page")
],
) -> UsersResponseDto:
"""
Get users page from the end.
Args:
page (int): Page number from the end (1 = last page).
size (int): Number of users per page.
Returns:
UsersResponseDto
"""
Optional[int],
Query(default=None, description="Page size for pagination")
] = None,
) -> GetAllUsersResponseDto:
"""Get all users"""
...
@delete("/users/{uuid}", response_class=DeleteUserResponseDto)
@ -64,84 +62,112 @@ class UsersController(BaseController):
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> DeleteUserResponseDto:
"""Delete User"""
"""Delete user"""
...
@post("users/{uuid}/actions/revoke", response_class=UserResponseDto)
@post("/users/{uuid}/actions/revoke", response_class=UpdateUserResponseDto)
async def revoke_user_subscription(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
body: Optional[Annotated[RevokeUserRequestDto, PydanticBody()]] = None,
) -> UserResponseDto:
) -> UpdateUserResponseDto:
"""Revoke User Subscription"""
...
@post("/users/{uuid}/actions/disable", response_class=UserResponseDto)
@post("/users/{uuid}/actions/disable", response_class=UpdateUserResponseDto)
async def disable_user(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> UserResponseDto:
) -> UpdateUserResponseDto:
"""Disable User"""
...
@post("/users/{uuid}/actions/enable", response_class=UserResponseDto)
@post("/users/{uuid}/actions/enable", response_class=UpdateUserResponseDto)
async def enable_user(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> UserResponseDto:
) -> UpdateUserResponseDto:
"""Enable User"""
...
@post("/users/{uuid}/actions/reset-traffic", response_class=UserResponseDto)
@post("/users/{uuid}/actions/reset-traffic", response_class=UpdateUserResponseDto)
async def reset_user_traffic(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> UserResponseDto:
) -> UpdateUserResponseDto:
"""Reset User Traffic"""
...
@get("/users/by-short-uuid/{short_uuid}", response_class=UserResponseDto)
async def get_user_by_short_uuid(
self,
short_uuid: Annotated[str, Path(description="Short UUID of the user")],
) -> UserResponseDto:
"""Get User By Short UUID"""
...
@get(
"/users/by-subscription-uuid/{subscription_uuid}",
response_class=UserResponseDto,
)
async def get_user_by_subscription_uuid(
self,
subscription_uuid: Annotated[str, Path(description="UUID of the subscription")],
) -> UserResponseDto:
"""Get User By Subscription UUID"""
...
@get("/users/{uuid}", response_class=UserResponseDto)
@get("/users/{uuid}", response_class=GetUserByUuidResponseDto)
async def get_user_by_uuid(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> UserResponseDto:
"""Get User By UUID"""
) -> GetUserByUuidResponseDto:
"""Get user by UUID"""
...
@get("/users/by-username/{username}", response_class=UserResponseDto)
async def get_user_by_username(
@get("/users/tags", response_class=GetAllTagsResponseDto)
async def get_all_tags(
self,
username: Annotated[str, Path(description="Username of the user")],
) -> UserResponseDto:
"""Get User By Username"""
) -> GetAllTagsResponseDto:
"""Get all existing user tags"""
...
@get(
"/users/by-telegram-id/{telegram_id}",
"/users/{uuid}/accessible-nodes",
response_class=GetUserAccessibleNodesResponseDto,
)
async def get_user_accessible_nodes(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> GetUserAccessibleNodesResponseDto:
"""Get user accessible nodes"""
...
@get(
"/users/{uuid}/subscription-request-history",
response_class=GetUserSubscriptionRequestHistoryResponseDto
)
async def get_user_subscription_request_history(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> GetUserSubscriptionRequestHistoryResponseDto:
"""Get user subscription request history, recent 24 records"""
...
# ИСПРАВЛЕНО: убран alias, используется short_uuid
@get("/users/by-short-uuid/{shortUuid}", response_class=GetUserByShortUuidResponseDto)
async def get_user_by_short_uuid(
self,
short_uuid: Annotated[str, Path(description="Short UUID of the user", alias="shortUuid")],
) -> GetUserByShortUuidResponseDto:
"""Get user by Short UUID"""
...
@get("/users/by-username/{username}", response_class=GetUserByUsernameResponseDto)
async def get_user_by_username(
self,
username: Annotated[str, Path(description="Username of the user")],
) -> GetUserByUsernameResponseDto:
"""Get user by username"""
...
@get("/users/by-id/{id}", response_class=GetUserByIdResponseDto)
async def get_user_by_id(
self,
id: Annotated[str, Path(description="ID of the user")],
) -> GetUserByIdResponseDto:
"""Get user by ID"""
...
# ИСПРАВЛЕНО: убран alias, используется telegram_id
@get(
"/users/by-telegram-id/{telegramId}",
response_class=TelegramUserResponseDto,
)
async def get_users_by_telegram_id(
self,
telegram_id: Annotated[str, Path(description="Telegram ID of the user")],
telegram_id: Annotated[str, Path(description="Telegram ID of the user", alias="telegramId")],
) -> TelegramUserResponseDto:
"""Get Users By Telegram ID"""
...
@ -160,30 +186,4 @@ class UsersController(BaseController):
tag: Annotated[str, Path(description="Tag of the user")],
) -> TagUserResponseDto:
"""Get Users By Tag"""
...
@get("/users/tags", response_class=TagsResponseDto)
async def get_all_tags(
self,
) -> TagsResponseDto:
"""Get All Tags"""
...
@get(
"/users/{uuid}/accessible-nodes",
response_class=GetUserAccessibleNodesResponseDto,
)
async def get_user_accessible_nodes(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> GetUserAccessibleNodesResponseDto:
"""Get User Accessible Nodes"""
...
@get("/users/{uuid}/subscription-request-history", response_class=GetSubscriptionRequestsResponseDto)
async def get_subscription_requests(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> GetSubscriptionRequestsResponseDto:
"""Get Subscription Requests History"""
...

View file

@ -5,71 +5,95 @@ from rapid_api_client.annotations import PydanticBody
from remnawave.enums import UserStatus
from remnawave.models import (
BulkAllExtendExpirationDateRequestDto,
BulkAllExtendExpirationDateResponseDto,
BulkAllResetTrafficUsersResponseDto,
BulkAllUpdateUsersRequestDto,
BulkAllUpdateUsersResponseDto,
BulkResponseDto,
BulkUpdateUsersInternalSquadsRequestDto,
UpdateUserFields,
BulkDeleteUsersByStatusRequestDto,
BulkDeleteUsersByStatusResponseDto,
BulkDeleteUsersRequestDto,
BulkDeleteUsersResponseDto,
BulkExtendExpirationDateRequestDto,
BulkExtendExpirationDateResponseDto,
BulkResetTrafficUsersRequestDto,
BulkResetTrafficUsersResponseDto,
BulkRevokeUsersSubscriptionRequestDto,
BulkRevokeUsersSubscriptionResponseDto,
BulkUpdateUsersRequestDto,
BulkUpdateUsersResponseDto,
BulkUpdateUsersSquadsRequestDto,
BulkUpdateUsersSquadsResponseDto,
)
from remnawave.rapid import AttributeBody, BaseController, patch, post
from remnawave.rapid import BaseController, post
class UsersBulkActionsController(BaseController):
@post(
"/users/bulk/delete-by-status",
response_class=BulkResponseDto,
response_class=BulkDeleteUsersByStatusResponseDto,
)
async def bulk_delete_users_by_status(
self, status: Annotated[UserStatus, AttributeBody()]
) -> BulkResponseDto:
self,
body: Annotated[BulkDeleteUsersByStatusRequestDto, PydanticBody()]
) -> BulkDeleteUsersByStatusResponseDto:
"""Bulk Delete Users By Status"""
...
@post("/users/bulk/delete", response_class=BulkResponseDto)
@post("/users/bulk/delete", response_class=BulkDeleteUsersResponseDto)
async def bulk_delete_users(
self,
uuids: Annotated[List[UUID], AttributeBody()],
) -> BulkResponseDto:
body: Annotated[BulkDeleteUsersRequestDto, PydanticBody()],
) -> BulkDeleteUsersResponseDto:
"""Bulk Delete Users By UUIDs"""
...
@post(
"/users/bulk/revoke-subscription",
response_class=BulkResponseDto,
response_class=BulkRevokeUsersSubscriptionResponseDto,
)
async def bulk_revoke_users_subscription(
self,
uuids: Annotated[List[UUID], AttributeBody()],
) -> BulkResponseDto:
body: Annotated[BulkRevokeUsersSubscriptionRequestDto, PydanticBody()],
) -> BulkRevokeUsersSubscriptionResponseDto:
"""Bulk Revoke Users Subscription"""
...
@post("/users/bulk/reset-traffic", response_class=BulkResponseDto)
@post("/users/bulk/reset-traffic", response_class=BulkResetTrafficUsersResponseDto)
async def bulk_reset_user_traffic(
self,
uuids: Annotated[List[UUID], AttributeBody()],
) -> BulkResponseDto:
body: Annotated[BulkResetTrafficUsersRequestDto, PydanticBody()],
) -> BulkResetTrafficUsersResponseDto:
"""Bulk Reset User Traffic"""
...
@post("/users/bulk/update", response_class=BulkResponseDto)
@post("/users/bulk/update", response_class=BulkUpdateUsersResponseDto)
async def bulk_update_users(
self,
uuids: Annotated[List[UUID], AttributeBody()],
fields: Annotated[UpdateUserFields, AttributeBody()],
) -> BulkResponseDto:
body: Annotated[BulkUpdateUsersRequestDto, PydanticBody()],
) -> BulkUpdateUsersResponseDto:
"""Bulk Update Users"""
...
@post("/users/bulk/update-squads", response_class=BulkResponseDto)
@post("/users/bulk/update-squads", response_class=BulkUpdateUsersSquadsResponseDto)
async def bulk_update_users_internal_squads(
self,
body: Annotated[BulkUpdateUsersInternalSquadsRequestDto, PydanticBody()],
) -> BulkResponseDto:
body: Annotated[BulkUpdateUsersSquadsRequestDto, PydanticBody()],
) -> BulkUpdateUsersSquadsResponseDto:
"""Bulk Update Users Internal Squads"""
...
@post(
"/users/bulk/extend-expiration-date",
response_class=BulkExtendExpirationDateResponseDto,
)
async def bulk_extend_expiration_date(
self,
body: Annotated[BulkExtendExpirationDateRequestDto, PydanticBody()],
) -> BulkExtendExpirationDateResponseDto:
"""Bulk Extend Users Expiration Date"""
...
@post("/users/bulk/all/update", response_class=BulkAllUpdateUsersResponseDto)
async def bulk_update_all_users(
self,
@ -89,13 +113,12 @@ class UsersBulkActionsController(BaseController):
...
@post(
"/users/bulk/update-squads",
response_class=BulkResponseDto,
)
async def bulk_update_users_internal_squad(
"/users/bulk/all/extend-expiration-date",
response_class=BulkAllExtendExpirationDateResponseDto,
)
async def bulk_all_extend_expiration_date(
self,
uuids: Annotated[List[UUID], AttributeBody()],
active_internal_squads: Annotated[List[UUID], AttributeBody(serialization_alias="activeInternalSquads")],
) -> BulkResponseDto:
"""Bulk Update Users External Squad"""
body: Annotated[BulkAllExtendExpirationDateRequestDto, PydanticBody()],
) -> BulkAllExtendExpirationDateResponseDto:
"""Bulk Extend All Users Expiration Date"""
...

View file

@ -190,6 +190,7 @@ from .subscription import (
SubscriptionInfoResponseDto, # Legacy alias
UserSubscription,
GetRawSubscriptionByShortUuidResponseDto,
RawSettings,
GetSubscriptionByShortUUIDResponseDto,
GetSubscriptionByUUIDResponseDto,
)
@ -203,6 +204,8 @@ from .subscriptions_settings import (
SubscriptionSettingsResponseDto,
UpdateSubscriptionSettingsRequestDto,
UpdateSubscriptionSettingsResponseDto,
CustomRemarks,
HwidSettings,
)
from .subscriptions_template import (
CreateSubscriptionTemplateRequestDto,
@ -239,27 +242,80 @@ from .system import (
EncryptHappCryptoLinkResponseDto,
)
from .users import (
ActiveInternalSquadDto,
# Request DTOs
CreateUserRequestDto,
DeleteUserResponseDto,
EmailUserResponseDto,
TelegramUserResponseDto,
UpdateUserRequestDto,
UserActiveInboundsDto,
UserLastConnectedNodeDto,
RevokeUserRequestDto,
# Response DTOs - Single User
CreateUserResponseDto,
UpdateUserResponseDto,
GetUserByUuidResponseDto,
GetUserByShortUuidResponseDto,
GetUserByUsernameResponseDto,
GetUserByIdResponseDto,
DisableUserResponseDto,
EnableUserResponseDto,
ResetUserTrafficResponseDto,
RevokeUserSubscriptionResponseDto,
ActivateAllInboundsResponseDto,
# Response DTOs - Collections
GetAllUsersResponseDto,
GetAllTagsResponseDto,
GetUserSubscriptionRequestHistoryResponseDto,
# Response DTOs - Arrays (RootModel)
TelegramUserResponseDto,
EmailUserResponseDto,
TagUserResponseDto,
# Other Response DTOs
DeleteUserResponseDto,
# Base DTOs
UserResponseDto,
UsersResponseDto,
TagsResponseDto,
TagUserResponseDto,
RevokeUserRequestDto,
SubscriptionRequestsResponseData,
# Data DTOs
UserTrafficDto,
ActiveInternalSquadDto,
SubscriptionRequestRecord,
HappCrypto,
UserActiveInboundsDto,
UserLastConnectedNodeDto,
# Alias
GetSubscriptionRequestsResponseDto,
)
from .users_bulk_actions import (
BulkAllExtendExpirationDateRequestDto,
BulkAllExtendExpirationDateResponseDto,
BulkAllResetTrafficUsersResponseDto,
BulkAllUpdateUsersRequestDto,
BulkAllUpdateUsersResponseDto,
BulkDeleteUsersByStatusRequestDto,
BulkDeleteUsersByStatusResponseDto,
BulkDeleteUsersRequestDto,
BulkDeleteUsersResponseDto,
BulkEventResponseData,
BulkExtendExpirationDateRequestDto,
BulkExtendExpirationDateResponseDto,
BulkResetTrafficUsersRequestDto,
BulkResetTrafficUsersResponseDto,
BulkResponseData,
BulkResponseDto,
BulkRevokeUsersSubscriptionRequestDto,
BulkRevokeUsersSubscriptionResponseDto,
BulkUpdateUsersInternalSquadsRequestDto,
BulkUpdateUsersRequestDto,
BulkUpdateUsersResponseDto,
BulkUpdateUsersSquadsRequestDto,
BulkUpdateUsersSquadsResponseDto,
TagStr,
UpdateUserFields,
)
from .users_stats import UserUsageByRange, UserUsageByRangeResponseDto
@ -427,10 +483,13 @@ __all__ = [
"SubscriptionInfoResponseDto", # Legacy alias
"UserSubscription",
"GetRawSubscriptionByShortUuidResponseDto",
"RawSettings",
# Subscription settings models
"GetSubscriptionSettingsResponseDto",
"SubscriptionSettingsResponseDto",
"UpdateSubscriptionSettingsRequestDto",
"CustomRemarks",
"HwidSettings",
"UpdateSubscriptionSettingsResponseDto",
"ResponseModificationHeader",
"ResponseModifications",
@ -514,40 +573,63 @@ __all__ = [
"SetPortToManyHostsResponseDto",
"SetPortToManyHostsRequestDto",
# Users models
"ActiveInternalSquadDto",
"CreateUserRequestDto",
"CreateUserResponseDto",
"DeleteUserResponseDto",
"DisableUserResponseDto",
"EmailUserResponseDto",
"EnableUserResponseDto",
"GetAllUsersResponseDto",
"GetUserByEmailResponseDto",
"GetUserByShortUuidResponseDto",
"GetUserBySubscriptionUuidResponseDto",
"GetUserByTagResponseDto",
"GetUserByTelegramIdResponseDto",
"GetUserByUsernameResponseDto",
"GetUserByUuidResponseDto",
"ResetUserTrafficResponseDto",
"RevokeUserRequestDto",
"RevokeUserSubscriptionResponseDto",
"TagsResponseDto",
"TelegramUserResponseDto",
"UpdateUserRequestDto",
"RevokeUserRequestDto",
"CreateUserResponseDto",
"UpdateUserResponseDto",
"UserActiveInboundsDto",
"UserLastConnectedNodeDto",
"GetUserByUuidResponseDto",
"GetUserByShortUuidResponseDto",
"GetUserByUsernameResponseDto",
"GetUserByIdResponseDto",
"DisableUserResponseDto",
"EnableUserResponseDto",
"ResetUserTrafficResponseDto",
"RevokeUserSubscriptionResponseDto",
"ActivateAllInboundsResponseDto",
"GetAllUsersResponseDto",
"GetAllTagsResponseDto",
"GetUserSubscriptionRequestHistoryResponseDto",
"TelegramUserResponseDto",
"EmailUserResponseDto",
"TagUserResponseDto",
"DeleteUserResponseDto",
"UserResponseDto",
"UsersResponseDto",
"TagUserResponseDto",
"TagsResponseDto",
"SubscriptionRequestsResponseData",
"UserTrafficDto",
"ActiveInternalSquadDto",
"SubscriptionRequestRecord",
"HappCrypto",
"UserActiveInboundsDto",
"UserLastConnectedNodeDto",
"GetSubscriptionRequestsResponseDto",
# Users bulk actions models
"BulkAllExtendExpirationDateRequestDto",
"BulkAllExtendExpirationDateResponseDto",
"BulkAllResetTrafficUsersResponseDto",
"BulkAllUpdateUsersRequestDto",
"BulkAllUpdateUsersResponseDto",
"BulkDeleteUsersByStatusRequestDto",
"BulkDeleteUsersByStatusResponseDto",
"BulkDeleteUsersRequestDto",
"BulkDeleteUsersResponseDto",
"BulkEventResponseData",
"BulkExtendExpirationDateRequestDto",
"BulkExtendExpirationDateResponseDto",
"BulkResetTrafficUsersRequestDto",
"BulkResetTrafficUsersResponseDto",
"BulkResponseData",
"BulkResponseDto",
"BulkRevokeUsersSubscriptionRequestDto",
"BulkRevokeUsersSubscriptionResponseDto",
"BulkUpdateUsersInternalSquadsRequestDto",
"BulkUpdateUsersRequestDto",
"BulkUpdateUsersResponseDto",
"BulkUpdateUsersSquadsRequestDto",
"BulkUpdateUsersSquadsResponseDto",
"TagStr",
"UpdateUserFields",
# Users stats models
"UserUsageByRange",

View file

@ -34,7 +34,6 @@ class ExternalSquadSubscriptionSettingsDto(BaseModel):
profile_update_interval: int = Field(alias="profileUpdateInterval", ge=1)
is_profile_webpage_url_enabled: bool = Field(alias="isProfileWebpageUrlEnabled")
serve_json_at_base_subscription: bool = Field(alias="serveJsonAtBaseSubscription")
add_username_to_base_subscription: bool = Field(alias="addUsernameToBaseSubscription")
is_show_custom_remarks: bool = Field(alias="isShowCustomRemarks")
happ_announce: Optional[str] = Field(None, alias="happAnnounce")
happ_routing: Optional[str] = Field(None, alias="happRouting")
@ -64,7 +63,8 @@ class ExternalSquadDto(BaseModel):
# Request/Response models
class GetExternalSquadsResponseDto(ExternalSquadDto):
"""Response with all external squads"""
pass
total: int = Field(alias="total")
external_squads: List[ExternalSquadDto] = Field(alias="externalSquads")
class GetExternalSquadByUuidResponseDto(ExternalSquadDto):

View file

@ -1,4 +1,4 @@
from typing import Annotated, List, Optional
from typing import Annotated, Any, Dict, List, Optional
from uuid import UUID
from pydantic import BaseModel, Field, StringConstraints, RootModel
@ -6,14 +6,6 @@ from pydantic import BaseModel, Field, StringConstraints, RootModel
from remnawave.enums import ALPN, Fingerprint, SecurityLayer
class ReorderResponse(BaseModel):
is_updated: bool = Field(alias="isUpdated")
class DeleteResponse(BaseModel):
is_deleted: bool = Field(alias="isDeleted")
class ReorderHostItem(BaseModel):
view_position: int = Field(serialization_alias="viewPosition")
uuid: UUID
@ -24,8 +16,8 @@ class ReorderHostRequestDto(BaseModel):
class HostInboundData(BaseModel):
config_profile_uuid: Optional[UUID] = Field(alias="configProfileUuid")
config_profile_inbound_uuid: Optional[UUID] = Field(alias="configProfileInboundUuid")
config_profile_uuid: Optional[UUID] = Field(None, alias="configProfileUuid")
config_profile_inbound_uuid: Optional[UUID] = Field(None, alias="configProfileInboundUuid")
class CreateHostInboundData(BaseModel):
@ -46,50 +38,21 @@ class UpdateHostRequestDto(BaseModel):
fingerprint: Optional[Fingerprint] = None
allow_insecure: Optional[bool] = Field(None, serialization_alias="allowInsecure")
is_disabled: Optional[bool] = Field(None, serialization_alias="isDisabled")
security_layer: Optional[SecurityLayer] = Field(
None, serialization_alias="securityLayer"
)
server_description: Optional[str] = Field(
None, serialization_alias="serverDescription", max_length=30
)
security_layer: Optional[SecurityLayer] = Field(None, serialization_alias="securityLayer")
server_description: Optional[str] = Field(None, serialization_alias="serverDescription", max_length=30)
tag: Optional[Annotated[str, StringConstraints(max_length=32, pattern=r"^[A-Z0-9_:]+$")]] = None
is_hidden: Optional[bool] = Field(
None,
serialization_alias="isHidden",
)
override_sni_from_address: Optional[bool] = Field(
None,
serialization_alias="overrideSniFromAddress",
)
vless_route_id: Optional[int] = Field(
None,
serialization_alias="vlessRouteId",
ge=0,
le=65535
)
shuffle_host: Optional[bool] = Field(
None,
serialization_alias="shuffleHost",
)
mihomo_x25519: Optional[bool] = Field(
None,
serialization_alias="mihomoX25519",
)
x_http_extra_params: Optional[str] = Field(
None,
serialization_alias="xHttpExtraParams",
)
mux_params: Optional[str] = Field(
None,
serialization_alias="muxParams",
)
sockopt_params: Optional[str] = Field(
None,
serialization_alias="sockoptParams",
)
is_hidden: Optional[bool] = Field(None, serialization_alias="isHidden")
override_sni_from_address: Optional[bool] = Field(None, serialization_alias="overrideSniFromAddress")
vless_route_id: Optional[int] = Field(None, serialization_alias="vlessRouteId", ge=0, le=65535)
shuffle_host: Optional[bool] = Field(None, serialization_alias="shuffleHost")
mihomo_x25519: Optional[bool] = Field(None, serialization_alias="mihomoX25519")
x_http_extra_params: Optional[Dict[str, Any]] = Field(None, serialization_alias="xHttpExtraParams")
mux_params: Optional[Dict[str, Any]] = Field(None, serialization_alias="muxParams")
sockopt_params: Optional[Dict[str, Any]] = Field(None, serialization_alias="sockoptParams")
nodes: Optional[List[UUID]] = None
xray_json_template_uuid: Optional[UUID] = Field(None, serialization_alias="xrayJsonTemplateUuid")
excluded_internal_squads: Optional[List[UUID]] = Field(None, serialization_alias="excludedInternalSquads")
# Legacy compatibility properties
@property
def inbound_uuid(self) -> Optional[UUID]:
return self.inbound.config_profile_inbound_uuid if self.inbound else None
@ -104,54 +67,26 @@ class HostResponseDto(BaseModel):
path: Optional[str] = None
sni: Optional[str] = None
host: Optional[str] = None
alpn: Optional[ALPN] = None
fingerprint: Optional[Fingerprint] = None
x_http_extra_params: Optional[str] = Field(
None,
alias="xHttpExtraParams",
)
mux_params: Optional[str] = Field(
None,
alias="muxParams",
)
sockopt_params: Optional[str] = Field(
None,
alias="sockoptParams",
)
alpn: Optional[str] = None
fingerprint: Optional[str] = None
x_http_extra_params: Optional[Dict[str, Any]] = Field(None, alias="xHttpExtraParams")
mux_params: Optional[Dict[str, Any]] = Field(None, alias="muxParams")
sockopt_params: Optional[Dict[str, Any]] = Field(None, alias="sockoptParams")
inbound: HostInboundData
server_description: Optional[str] = Field(
None, alias="serverDescription"
)
server_description: Optional[str] = Field(None, alias="serverDescription")
tag: Optional[str] = None
vless_route_id: Optional[int] = Field(
None,
alias="vlessRouteId",
)
vless_route_id: Optional[int] = Field(None, alias="vlessRouteId")
shuffle_host: bool = Field(alias="shuffleHost")
mihomo_x25519: bool = Field(alias="mihomoX25519")
nodes: List[UUID]
is_disabled: bool = Field(
default=False,
alias="isDisabled",
)
security_layer: SecurityLayer = Field(
default=SecurityLayer.DEFAULT,
alias="securityLayer",
)
is_hidden: bool = Field(
default=False,
alias="isHidden",
)
override_sni_from_address: bool = Field(
default=False,
alias="overrideSniFromAddress",
)
allow_insecure: bool = Field(
default=False,
alias="allowInsecure",
)
is_disabled: bool = Field(False, alias="isDisabled")
security_layer: SecurityLayer = Field(SecurityLayer.DEFAULT, alias="securityLayer")
is_hidden: bool = Field(False, alias="isHidden")
override_sni_from_address: bool = Field(False, alias="overrideSniFromAddress")
allow_insecure: bool = Field(False, alias="allowInsecure")
xray_json_template_uuid: Optional[UUID] = Field(None, alias="xrayJsonTemplateUuid")
excluded_internal_squads: List[UUID] = Field(default_factory=list, alias="excludedInternalSquads")
# Legacy compatibility property
@property
def inbound_uuid(self) -> Optional[UUID]:
return self.inbound.config_profile_inbound_uuid
@ -167,64 +102,27 @@ class CreateHostRequestDto(BaseModel):
host: Optional[str] = None
alpn: Optional[ALPN] = None
fingerprint: Optional[Fingerprint] = None
x_http_extra_params: Optional[str] = Field(
None,
serialization_alias="xHttpExtraParams",
)
mux_params: Optional[str] = Field(
None,
serialization_alias="muxParams",
)
sockopt_params: Optional[str] = Field(
None,
serialization_alias="sockoptParams",
)
server_description: Optional[str] = Field(
None, serialization_alias="serverDescription", max_length=30
)
x_http_extra_params: Optional[Dict[str, Any]] = Field(None, serialization_alias="xHttpExtraParams")
mux_params: Optional[Dict[str, Any]] = Field(None, serialization_alias="muxParams")
sockopt_params: Optional[Dict[str, Any]] = Field(None, serialization_alias="sockoptParams")
server_description: Optional[str] = Field(None, serialization_alias="serverDescription", max_length=30)
tag: Optional[Annotated[str, StringConstraints(max_length=32, pattern=r"^[A-Z0-9_:]+$")]] = None
vless_route_id: Optional[int] = Field(
None,
serialization_alias="vlessRouteId",
ge=0,
le=65535
)
shuffle_host: bool = Field(
False,
serialization_alias="shuffleHost",
)
mihomo_x25519: bool = Field(
False,
serialization_alias="mihomoX25519",
)
vless_route_id: Optional[int] = Field(None, serialization_alias="vlessRouteId", ge=0, le=65535)
shuffle_host: bool = Field(False, serialization_alias="shuffleHost")
mihomo_x25519: bool = Field(False, serialization_alias="mihomoX25519")
nodes: List[UUID] = Field(default_factory=list)
allow_insecure: bool = Field(
False,
serialization_alias="allowInsecure",
)
is_disabled: bool = Field(
False,
serialization_alias="isDisabled",
)
security_layer: SecurityLayer = Field(
SecurityLayer.DEFAULT,
serialization_alias="securityLayer",
)
is_hidden: bool = Field(
False,
serialization_alias="isHidden",
)
override_sni_from_address: bool = Field(
False,
serialization_alias="overrideSniFromAddress",
)
allow_insecure: bool = Field(False, serialization_alias="allowInsecure")
is_disabled: bool = Field(False, serialization_alias="isDisabled")
security_layer: SecurityLayer = Field(SecurityLayer.DEFAULT, serialization_alias="securityLayer")
is_hidden: bool = Field(False, serialization_alias="isHidden")
override_sni_from_address: bool = Field(False, serialization_alias="overrideSniFromAddress")
xray_json_template_uuid: Optional[UUID] = Field(None, serialization_alias="xrayJsonTemplateUuid")
excluded_internal_squads: List[UUID] = Field(default_factory=list, serialization_alias="excludedInternalSquads")
# Legacy compatibility property
@property
def inbound_uuid(self) -> Optional[UUID]:
return self.inbound.config_profile_inbound_uuid
# Constructor compatibility - support old-style inbound_uuid
def __init__(
self,
inbound_uuid: Optional[UUID] = None,
@ -232,8 +130,6 @@ class CreateHostRequestDto(BaseModel):
**data,
):
if inbound_uuid is not None and "inbound" not in data:
# Legacy mode: create inbound object from UUID
# Use hardcoded config_profile_uuid from API response for compatibility
data["inbound"] = CreateHostInboundData(
config_profile_uuid=config_profile_uuid
or UUID("107541f1-ae1a-4e2d-9dec-7297557b5125"),
@ -246,11 +142,14 @@ class GetAllHostTagsResponseDto(BaseModel):
tags: List[str]
# Response wrappers - обернуты в response
class CreateHostResponseDto(HostResponseDto):
"""Create host response"""
pass
class UpdateHostResponseDto(HostResponseDto):
class UpdateHostResponseDto(CreateHostResponseDto):
"""Update host response"""
pass
@ -265,16 +164,23 @@ class GetAllHostsResponseDto(RootModel[List[HostResponseDto]]):
class GetOneHostResponseDto(HostResponseDto):
"""Get one host response"""
pass
class ReorderHostResponseDto(BaseModel):
"""Reorder hosts response"""
is_updated: bool = Field(alias="isUpdated", default=True)
class DeleteHostResponseDto(BaseModel):
"""Delete host response"""
is_deleted: bool = Field(alias="isDeleted")
# Legacy compatibility
HostsResponseDto = List[HostResponseDto]
class HostsResponseDto(HostResponseDto):
"""Host response data with backward compatibility properties"""
@property
def allow_insecure(self) -> bool:
"""Backward compatibility property"""
return self.security_layer == SecurityLayer.NONE

View file

@ -125,8 +125,6 @@ class NodeResponseDto(BaseModel):
is_connected: bool = Field(alias="isConnected")
is_disabled: bool = Field(alias="isDisabled")
is_connecting: bool = Field(alias="isConnecting")
is_node_online: bool = Field(alias="isNodeOnline")
is_xray_running: bool = Field(alias="isXrayRunning")
last_status_change: Optional[datetime] = Field(None, alias="lastStatusChange")
last_status_message: Optional[str] = Field(None, alias="lastStatusMessage")
xray_version: Optional[str] = Field(None, alias="xrayVersion")

View file

@ -4,11 +4,17 @@ from uuid import UUID
from pydantic import BaseModel, Field, RootModel
class NodeActiveSquadDto(BaseModel):
squad_name: str = Field(alias="squadName")
active_inbounds: list[str] = Field(alias="activeInbounds")
class NodeInfoDto(BaseModel):
uuid: UUID
name: str
country_code: str = Field(alias="countryCode")
config_profile_name: str = Field(alias="configProfileName")
config_profile_uuid: UUID = Field(alias="configProfileUuid")
active_squads: List[NodeActiveSquadDto] = Field(alias="activeSquads")
class GetUserAccessibleNodesResponse(BaseModel):
user_uuid: UUID = Field(alias="userUuid")

View file

@ -69,9 +69,9 @@ class RemnawaveSettingsData(BaseModel):
branding_settings: Optional[BrandingSettings] = Field(None, alias="brandingSettings")
class GetRemnawaveSettingsResponseDto(BaseModel):
class GetRemnawaveSettingsResponseDto(RemnawaveSettingsData):
"""Get Remnawave settings response"""
response: RemnawaveSettingsData
pass
class UpdateRemnawaveSettingsRequestDto(BaseModel):
@ -83,6 +83,6 @@ class UpdateRemnawaveSettingsRequestDto(BaseModel):
branding_settings: Optional[BrandingSettings] = Field(None, serialization_alias="brandingSettings")
class UpdateRemnawaveSettingsResponseDto(BaseModel):
class UpdateRemnawaveSettingsResponseDto(RemnawaveSettingsData):
"""Update Remnawave settings response"""
response: RemnawaveSettingsData
pass

View file

@ -5,6 +5,7 @@ from uuid import UUID
from pydantic import BaseModel, Field
from remnawave.enums import TrafficLimitStrategy, UserStatus
from remnawave.utils.happ_crypt import create_happ_crypto_link
class HappCrypto(BaseModel):
@ -22,22 +23,24 @@ class ActiveInternalSquadDto(BaseModel):
name: str
class UserTrafficDto(BaseModel):
"""User traffic information"""
used_traffic_bytes: float = Field(alias="usedTrafficBytes")
lifetime_used_traffic_bytes: float = Field(alias="lifetimeUsedTrafficBytes")
online_at: Optional[datetime] = Field(None, alias="onlineAt")
first_connected_at: Optional[datetime] = Field(None, alias="firstConnectedAt")
last_connected_node_uuid: Optional[UUID] = Field(None, alias="lastConnectedNodeUuid")
class UserResponseDto(BaseModel):
uuid: UUID
short_uuid: str = Field(alias="shortUuid")
username: str
status: UserStatus = Field(default=UserStatus.ACTIVE)
used_traffic_bytes: float = Field(alias="usedTrafficBytes")
lifetime_used_traffic_bytes: float = Field(alias="lifetimeUsedTrafficBytes")
traffic_limit_bytes: int = Field(default=0, alias="trafficLimitBytes")
traffic_limit_strategy: TrafficLimitStrategy = Field(
default=TrafficLimitStrategy.NO_RESET,
alias="trafficLimitStrategy"
)
user_traffic: UserTrafficDto = Field(alias="userTraffic")
sub_last_user_agent: Optional[str] = Field(None, alias="subLastUserAgent")
sub_last_opened_at: Optional[datetime] = Field(None, alias="subLastOpenedAt")
expire_at: datetime = Field(alias="expireAt")
online_at: Optional[datetime] = Field(None, alias="onlineAt")
sub_revoked_at: Optional[datetime] = Field(None, alias="subRevokedAt")
last_traffic_reset_at: Optional[datetime] = Field(None, alias="lastTrafficResetAt")
trojan_password: str = Field(alias="trojanPassword")
@ -48,14 +51,37 @@ class UserResponseDto(BaseModel):
telegram_id: Optional[int] = Field(None, alias="telegramId")
email: Optional[str] = None
hwid_device_limit: Optional[int] = Field(None, alias="hwidDeviceLimit")
first_connected_at: Optional[datetime] = Field(None, alias="firstConnectedAt")
last_triggered_threshold: int = Field(default=0, alias="lastTriggeredThreshold")
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
active_internal_squads: List[ActiveInternalSquadDto] = Field(alias="activeInternalSquads")
subscription_url: str = Field(alias="subscriptionUrl")
last_connected_node: Optional[UserLastConnectedNodeDto] = Field(None, alias="lastConnectedNode")
happ: HappCrypto = Field(alias="happ")
# Legacy alias for backward compatibility
@property
def used_traffic_bytes(self) -> float:
"""Backward compatibility property"""
return self.user_traffic.used_traffic_bytes
@property
def lifetime_used_traffic_bytes(self) -> float:
"""Backward compatibility property"""
return self.user_traffic.lifetime_used_traffic_bytes
@property
def online_at(self) -> Optional[datetime]:
"""Backward compatibility property"""
return self.user_traffic.online_at
@property
def first_connected_at(self) -> Optional[datetime]:
"""Backward compatibility property"""
return self.user_traffic.first_connected_at
@property
def last_connected_node_uuid(self) -> Optional[UUID]:
"""Backward compatibility property"""
return self.user_traffic.last_connected_node_uuid
class ConvertedUserInfo(BaseModel):
@ -85,11 +111,11 @@ class RawHostProtocolOptions(BaseModel):
class RawHostDbData(BaseModel):
raw_inbound: Optional[Dict[str, Any]] = Field(alias="rawInbound")
raw_inbound: Optional[Dict[str, Any]] = Field(None, alias="rawInbound")
inbound_tag: str = Field(alias="inboundTag")
uuid: str
config_profile_uuid: Optional[str] = Field(alias="configProfileUuid")
config_profile_inbound_uuid: Optional[str] = Field(alias="configProfileInboundUuid")
config_profile_uuid: Optional[str] = Field(None, alias="configProfileUuid")
config_profile_inbound_uuid: Optional[str] = Field(None, alias="configProfileInboundUuid")
is_disabled: bool = Field(alias="isDisabled")
view_position: int = Field(alias="viewPosition")
remark: str
@ -97,6 +123,10 @@ class RawHostDbData(BaseModel):
tag: Optional[str] = None
vless_route_id: Optional[int] = Field(None, alias="vlessRouteId")
class RawSettings(BaseModel):
"""Raw settings for network configuration"""
header_type: Optional[str] = Field(None, alias="headerType")
request: Optional[Dict[str, Any]] = None
class RawHost(BaseModel):
password: Passwords
@ -107,14 +137,14 @@ class RawHost(BaseModel):
network: Optional[str] = None
path: Optional[str] = None
public_key: Optional[str] = Field(None, alias="publicKey")
port: Optional[float] = None # изменен тип на float
port: Optional[float] = None
protocol: Optional[str] = None
remark: Optional[str] = None
short_id: Optional[str] = Field(None, alias="shortId")
sni: Optional[str] = None
spider_x: Optional[str] = Field(None, alias="spiderX")
tls: Optional[str] = None
header_type: Optional[str] = Field(None, alias="headerType")
raw_settings: Optional[RawSettings] = Field(None, alias="rawSettings")
additional_params: Optional[RawHostAdditionalParams] = Field(None, alias="additionalParams")
x_http_extra_params: Optional[Dict[str, Any]] = Field(None, alias="xHttpExtraParams")
mux_params: Optional[Dict[str, Any]] = Field(None, alias="muxParams")
@ -128,9 +158,11 @@ class RawHost(BaseModel):
encryption: Optional[str] = None
protocol_options: Optional[RawHostProtocolOptions] = Field(None, alias="protocolOptions")
db_data: RawHostDbData = Field(alias="dbData")
xray_json_template: Optional[Dict[str, Any]] = Field(None, alias="xrayJsonTemplate")
class RawSubscriptionResponse(BaseModel):
"""Raw subscription response data"""
user: UserResponseDto
converted_user_info: ConvertedUserInfo = Field(alias="convertedUserInfo")
headers: Dict[str, str]
@ -172,7 +204,12 @@ class GetSubscriptionInfoResponseDto(BaseModel):
links: List[str]
ss_conf_links: Dict[str, str] = Field(alias="ssConfLinks")
subscription_url: str = Field(alias="subscriptionUrl")
happ: HappCrypto
@property
def happ(self) -> HappCrypto:
"""Generate HAPP link on the fly"""
crypto_link = create_happ_crypto_link(self.subscription_url)
return HappCrypto(crypto_link=crypto_link)
class SubscriptionWithoutHapp(BaseModel):

View file

@ -55,29 +55,43 @@ class ResponseRules(BaseModel):
rules: List[ResponseRule]
class CustomRemarks(BaseModel):
"""Custom remarks for different user states"""
expired_users: List[str] = Field(alias="expiredUsers", min_length=1)
limited_users: List[str] = Field(alias="limitedUsers", min_length=1)
disabled_users: List[str] = Field(alias="disabledUsers", min_length=1)
empty_hosts: List[str] = Field(alias="emptyHosts", min_length=1)
empty_internal_squads: List[str] = Field(alias="emptyInternalSquads", min_length=1)
class HwidSettings(BaseModel):
"""HWID (Hardware ID) settings"""
enabled: bool
fallback_device_limit: int = Field(alias="fallbackDeviceLimit")
max_devices_announce: Optional[Annotated[str, StringConstraints(max_length=200)]] = Field(
None, alias="maxDevicesAnnounce"
)
class SubscriptionSettingsResponseDto(BaseModel):
"""Subscription settings response data"""
uuid: UUID
profile_title: str = Field(alias="profileTitle")
support_link: str = Field(alias="supportLink")
profile_update_interval: int = Field(
alias="profileUpdateInterval", strict=True, ge=1
)
profile_update_interval: int = Field(alias="profileUpdateInterval", ge=1)
is_profile_webpage_url_enabled: bool = Field(alias="isProfileWebpageUrlEnabled")
serve_json_at_base_subscription: bool = Field(alias="serveJsonAtBaseSubscription")
add_username_to_base_subscription: bool = Field(
alias="addUsernameToBaseSubscription"
)
show_custom_remarks: bool = Field(alias="isShowCustomRemarks")
custom_remarks: CustomRemarks = Field(alias="customRemarks")
happ_announce: Optional[str] = Field(None, alias="happAnnounce")
happ_routing: Optional[str] = Field(None, alias="happRouting")
expired_users_remarks: List[str] = Field(alias="expiredUsersRemarks")
limited_users_remarks: List[str] = Field(alias="limitedUsersRemarks")
disabled_users_remarks: List[str] = Field(alias="disabledUsersRemarks")
custom_response_headers: Optional[Dict[str, str]] = Field(
None, alias="customResponseHeaders"
)
custom_response_headers: Optional[Dict[str, str]] = Field(None, alias="customResponseHeaders")
randomize_hosts: bool = Field(alias="randomizeHosts")
response_rules: Optional[ResponseRules] = Field(None, alias="responseRules")
hwid_settings: Optional[HwidSettings] = Field(None, alias="hwidSettings")
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
@ -91,39 +105,29 @@ class UpdateSubscriptionSettingsResponseDto(SubscriptionSettingsResponseDto):
class UpdateSubscriptionSettingsRequestDto(BaseModel):
"""Update subscription settings request"""
uuid: UUID
profile_title: Optional[str] = Field(None, alias="profileTitle")
support_link: Optional[str] = Field(None, alias="supportLink")
profile_update_interval: Optional[int] = Field(
None, alias="profileUpdateInterval"
)
profile_title: Optional[str] = Field(None, serialization_alias="profileTitle")
support_link: Optional[str] = Field(None, serialization_alias="supportLink")
profile_update_interval: Optional[int] = Field(None, serialization_alias="profileUpdateInterval")
is_profile_webpage_url_enabled: Optional[bool] = Field(
None, alias="isProfileWebpageUrlEnabled"
None, serialization_alias="isProfileWebpageUrlEnabled"
)
serve_json_at_base_subscription: Optional[bool] = Field(
None, alias="serveJsonAtBaseSubscription"
None, serialization_alias="serveJsonAtBaseSubscription"
)
add_username_to_base_subscription: Optional[bool] = Field(
None, alias="addUsernameToBaseSubscription"
)
is_show_custom_remarks: Optional[bool] = Field(
None, alias="isShowCustomRemarks"
)
happ_announce: Annotated[Optional[str], StringConstraints(max_length=200)] = Field(
None, alias="happAnnounce"
)
happ_routing: Optional[str] = Field(None, alias="happRouting")
expired_users_remarks: Optional[List[str]] = Field(
None, alias="expiredUsersRemarks"
)
limited_users_remarks: Optional[List[str]] = Field(
None, alias="limitedUsersRemarks"
)
disabled_users_remarks: Optional[List[str]] = Field(
None, alias="disabledUsersRemarks"
is_show_custom_remarks: Optional[bool] = Field(None, serialization_alias="isShowCustomRemarks")
custom_remarks: Optional[CustomRemarks] = Field(None, serialization_alias="customRemarks")
happ_announce: Optional[Annotated[str, StringConstraints(max_length=200)]] = Field(
None, serialization_alias="happAnnounce"
)
happ_routing: Optional[str] = Field(None, serialization_alias="happRouting")
custom_response_headers: Optional[Dict[str, str]] = Field(
None, alias="customResponseHeaders"
None, serialization_alias="customResponseHeaders"
)
randomize_hosts: Optional[bool] = Field(None, alias="randomizeHosts")
response_rules: Optional[ResponseRules] = Field(None, alias="responseRules")
randomize_hosts: Optional[bool] = Field(None, serialization_alias="randomizeHosts")
response_rules: Optional[ResponseRules] = Field(None, serialization_alias="responseRules")
hwid_settings: Optional[HwidSettings] = Field(None, serialization_alias="hwidSettings")

View file

@ -45,16 +45,21 @@ class MemoryStatistic(BaseModel):
class StatusCounts(BaseModel):
active: int = Field(alias="ACTIVE")
disabled: int = Field(alias="DISABLED")
limited: int = Field(alias="LIMITED")
expired: int = Field(alias="EXPIRED")
"""Dynamic status counts - использует additionalProperties"""
model_config = {"extra": "allow"}
def __getitem__(self, key: str) -> int:
"""Allow dict-like access"""
return getattr(self, key, 0)
def get(self, key: str, default: int = 0) -> int:
"""Dict-like get method"""
return getattr(self, key, default)
class UsersStatistic(BaseModel):
status_counts: StatusCounts = Field(alias="statusCounts")
total_users: int = Field(alias="totalUsers")
total_traffic_bytes: int = Field(alias="totalTrafficBytes")
class OnlineStatistic(BaseModel):
@ -66,9 +71,11 @@ class OnlineStatistic(BaseModel):
class NodesStatistic(BaseModel):
total_online: int = Field(alias="totalOnline")
total_bytes_lifetime: str = Field(alias="totalBytesLifetime")
class StatisticResponseDto(BaseModel):
"""System statistics data"""
cpu: CPUStatistic
memory: MemoryStatistic
uptime: float
@ -89,6 +96,7 @@ class RemnawaveHealthData(BaseModel):
class GetStatsResponseDto(StatisticResponseDto):
"""Get system statistics response"""
pass
@ -108,7 +116,6 @@ class GetRemnawaveHealthResponseDto(BaseModel):
pm2_stats: List[PM2Stat] = Field(alias="pm2Stats")
class NodeMetric(BaseModel):
"""Node metric data"""
uuid: str = Field(alias="nodeUuid")
@ -147,8 +154,8 @@ class EncryptHappCryptoLinkData(BaseModel):
encrypted_link: str = Field(alias="encryptedLink")
class EncryptHappCryptoLinkResponseDto(BaseModel):
response: EncryptHappCryptoLinkData
class EncryptHappCryptoLinkResponseDto(EncryptHappCryptoLinkData):
pass
class DebugSrrMatcherRequestDto(BaseModel):
@ -163,5 +170,5 @@ class DebugSrrMatcherData(BaseModel):
output_headers: Dict[str, str] = Field(alias="outputHeaders")
class DebugSrrMatcherResponseDto(BaseModel):
response: DebugSrrMatcherData
class DebugSrrMatcherResponseDto(DebugSrrMatcherData):
pass

View file

@ -4,13 +4,14 @@ from uuid import UUID
from pydantic import (
BaseModel,
EmailStr,
Field,
RootModel,
StringConstraints,
)
from pydantic.alias_generators import to_camel
from remnawave.enums import TrafficLimitStrategy, UserStatus
from remnawave.utils.happ_crypt import create_happ_crypto_link
class UserActiveInboundsDto(BaseModel):
@ -32,193 +33,158 @@ class ActiveInternalSquadDto(BaseModel):
class HappCrypto(BaseModel):
cryptoLink: str
crypto_link: str = Field(alias="cryptoLink")
class CreateUserRequestDto(BaseModel):
expire_at: datetime = Field(..., serialization_alias="expireAt")
"""Request DTO for creating a user"""
# Required fields
username: Annotated[
str, StringConstraints(pattern=r"^[a-zA-Z0-9_-]+$", min_length=3, max_length=36)
]
created_at: datetime | None = Field(None, serialization_alias="createdAt")
status: UserStatus | None = None
short_uuid: str | None = Field(None, serialization_alias="shortUuid")
trojan_password: Annotated[
str | None, StringConstraints(min_length=8, max_length=32)
] = Field(None, serialization_alias="trojanPassword")
vless_uuid: str | None = Field(None, serialization_alias="vlessUuid")
ss_password: Annotated[
str | None, StringConstraints(min_length=8, max_length=32)
] = Field(None, serialization_alias="ssPassword")
traffic_limit_bytes: int | None = Field(
None, serialization_alias="trafficLimitBytes", strict=True, ge=0
str,
StringConstraints(pattern=r"^[a-zA-Z0-9_-]+$", min_length=3, max_length=36)
] = Field(..., description="Unique username for the user")
expire_at: datetime = Field(..., serialization_alias="expireAt", description="Account expiration date")
# Optional fields with defaults
status: UserStatus = Field(default=UserStatus.ACTIVE, description="User account status")
traffic_limit_strategy: TrafficLimitStrategy = Field(
default=TrafficLimitStrategy.NO_RESET,
serialization_alias="trafficLimitStrategy",
description="Traffic reset strategy"
)
traffic_limit_strategy: TrafficLimitStrategy | None = Field(
None, serialization_alias="trafficLimitStrategy"
# Optional fields
short_uuid: Optional[str] = Field(None, serialization_alias="shortUuid")
trojan_password: Optional[Annotated[str, StringConstraints(min_length=8, max_length=32)]] = Field(
None, serialization_alias="trojanPassword"
)
last_traffic_reset_at: datetime | None = Field(
None, serialization_alias="lastTrafficResetAt"
vless_uuid: Optional[UUID] = Field(None, serialization_alias="vlessUuid")
ss_password: Optional[Annotated[str, StringConstraints(min_length=8, max_length=32)]] = Field(
None, serialization_alias="ssPassword"
)
description: str | None = None
tag: str | None = None
telegram_id: int | None = Field(None, serialization_alias="telegramId")
email: str | None = None
hwid_device_limit: int | None = Field(
None, serialization_alias="hwidDeviceLimit", strict=True, ge=0
traffic_limit_bytes: Optional[int] = Field(
None, serialization_alias="trafficLimitBytes", ge=0
)
active_internal_squads: list[UUID] | None = Field(
None, serialization_alias="activeInternalSquads"
created_at: Optional[datetime] = Field(None, serialization_alias="createdAt")
last_traffic_reset_at: Optional[datetime] = Field(None, serialization_alias="lastTrafficResetAt")
description: Optional[str] = None
tag: Optional[Annotated[str, StringConstraints(max_length=16, pattern=r"^[A-Z0-9_]+$")]] = None
telegram_id: Optional[int] = Field(None, serialization_alias="telegramId")
email: Optional[EmailStr] = None
hwid_device_limit: Optional[int] = Field(None, serialization_alias="hwidDeviceLimit", ge=0)
active_internal_squads: Optional[List[UUID]] = Field(None, serialization_alias="activeInternalSquads")
uuid: Optional[UUID] = Field(
None,
description="Optional. Pass UUID to create user with specific UUID, otherwise it will be generated automatically."
)
external_squad_uuid: UUID | None = Field(None, serialization_alias="externalSquadUuid")
uuid: Optional[UUID] = Field(None, description="UUID of the user. Optional. If not provided, a new UUID will be generated by Remnawave.")
external_squad_uuid: Optional[UUID] = Field(None, serialization_alias="externalSquadUuid")
class UpdateUserRequestDto(BaseModel):
uuid: UUID
description: str | None = None
email: str | None = None
expire_at: datetime | None = Field(None, serialization_alias="expireAt")
hwid_device_limit: int | None = Field(
None, serialization_alias="hwidDeviceLimit", strict=True, ge=0
"""Request DTO for updating a user"""
# Either username or uuid must be provided, uuid has priority
username: Optional[str] = Field(None, description="Username of the user")
uuid: Optional[UUID] = Field(
None,
description="UUID of the user. UUID has higher priority than username"
)
status: UserStatus | None = None
tag: str | None = None
telegram_id: int | None = Field(None, serialization_alias="telegramId")
traffic_limit_bytes: int | None = Field(
None, serialization_alias="trafficLimitBytes", strict=True, ge=0
)
traffic_limit_strategy: TrafficLimitStrategy | None = Field(
# Optional update fields
status: Optional[UserStatus] = None
description: Optional[str] = None
email: Optional[EmailStr] = None
expire_at: Optional[datetime] = Field(None, serialization_alias="expireAt")
hwid_device_limit: Optional[int] = Field(None, serialization_alias="hwidDeviceLimit", ge=0)
tag: Optional[Annotated[str, StringConstraints(max_length=16, pattern=r"^[A-Z0-9_]+$")]] = None
telegram_id: Optional[int] = Field(None, serialization_alias="telegramId")
traffic_limit_bytes: Optional[int] = Field(None, serialization_alias="trafficLimitBytes", ge=0)
traffic_limit_strategy: Optional[TrafficLimitStrategy] = Field(
None, serialization_alias="trafficLimitStrategy"
)
active_internal_squads: list[UUID] | None = Field(
active_internal_squads: Optional[List[UUID]] = Field(
None, serialization_alias="activeInternalSquads"
)
external_squad_uuid: UUID | None = Field(None, alias="externalSquadUuid")
external_squad_uuid: Optional[UUID] = Field(None, serialization_alias="externalSquadUuid")
class UserTrafficDto(BaseModel):
"""User traffic information"""
used_traffic_bytes: float = Field(alias="usedTrafficBytes")
lifetime_used_traffic_bytes: float = Field(alias="lifetimeUsedTrafficBytes")
online_at: Optional[datetime] = Field(None, alias="onlineAt")
first_connected_at: Optional[datetime] = Field(None, alias="firstConnectedAt")
last_connected_node_uuid: Optional[UUID] = Field(None, alias="lastConnectedNodeUuid")
class UserResponseDto(BaseModel):
"""User response DTO - обновленная структура с userTraffic"""
uuid: UUID
short_uuid: str = Field(alias="shortUuid")
username: str
status: UserStatus | None = None
used_traffic_bytes: float = Field(alias="usedTrafficBytes")
lifetime_used_traffic_bytes: float = Field(alias="lifetimeUsedTrafficBytes")
traffic_limit_bytes: int | None = Field(None, alias="trafficLimitBytes")
traffic_limit_strategy: str | None = Field(None, alias="trafficLimitStrategy")
sub_last_user_agent: str | None = Field(None, alias="subLastUserAgent")
sub_last_opened_at: datetime | None = Field(None, alias="subLastOpenedAt")
expire_at: datetime | None = Field(None, alias="expireAt")
online_at: datetime | None = Field(None, alias="onlineAt")
sub_revoked_at: datetime | None = Field(None, alias="subRevokedAt")
last_traffic_reset_at: datetime | None = Field(None, alias="lastTrafficResetAt")
status: UserStatus = Field(default=UserStatus.ACTIVE)
traffic_limit_bytes: int = Field(0, alias="trafficLimitBytes")
traffic_limit_strategy: TrafficLimitStrategy = Field(
TrafficLimitStrategy.NO_RESET, alias="trafficLimitStrategy"
)
expire_at: datetime = Field(alias="expireAt")
telegram_id: Optional[int] = Field(None, alias="telegramId")
email: Optional[str] = None
description: Optional[str] = None
tag: Optional[str] = None
hwid_device_limit: Optional[int] = Field(None, alias="hwidDeviceLimit")
external_squad_uuid: Optional[UUID] = Field(None, alias="externalSquadUuid")
trojan_password: str = Field(alias="trojanPassword")
vless_uuid: UUID = Field(alias="vlessUuid")
ss_password: str = Field(alias="ssPassword")
description: str | None = None
telegram_id: int | None = Field(None, alias="telegramId")
email: str | None = None
hwid_device_limit: int | None = Field(
None, alias="hwidDeviceLimit", strict=True, ge=0
)
active_internal_squads: list[ActiveInternalSquadDto] | None = Field(
None, alias="activeInternalSquads"
)
subscription_url: str | None = Field(None, alias="subscriptionUrl")
first_connected: datetime | None = Field(None, alias="firstConnectedAt")
last_trigger_threshold: int | None = Field(None, alias="lastTriggeredThreshold")
last_connected_node: UserLastConnectedNodeDto | None = Field(
None, alias="lastConnectedNode"
)
happ: HappCrypto | None = Field(None, alias="happ")
tag: str | None = Field(None, alias="tag")
external_squad_uuid: UUID | None = Field(None, alias="externalSquadUuid")
last_trigger_threshold: int = Field(0, alias="lastTriggeredThreshold")
sub_revoked_at: Optional[datetime] = Field(None, alias="subRevokedAt")
sub_last_user_agent: Optional[str] = Field(None, alias="subLastUserAgent")
sub_last_opened_at: Optional[datetime] = Field(None, alias="subLastOpenedAt")
last_traffic_reset_at: Optional[datetime] = Field(None, alias="lastTrafficResetAt")
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
subscription_url: str = Field(alias="subscriptionUrl")
active_internal_squads: list[ActiveInternalSquadDto] = Field(alias="activeInternalSquads")
user_traffic: UserTrafficDto = Field(alias="userTraffic")
model_config = {"alias_generator": to_camel, "populate_by_name": True}
class EmailUserResponseDto(RootModel[list[UserResponseDto]]):
def __iter__(self):
return iter(self.root)
def __getitem__(self, item):
return self.root[item]
class TagUserResponseDto(RootModel[list[UserResponseDto]]):
def __iter__(self):
return iter(self.root)
def __getitem__(self, item):
return self.root[item]
class TelegramUserResponseDto(RootModel[list[UserResponseDto]]):
def __iter__(self):
return iter(self.root)
def __getitem__(self, item):
return self.root[item]
class UsersResponseDto(BaseModel):
users: list[UserResponseDto]
total: int
class DeleteUserResponseDto(BaseModel):
is_deleted: bool = Field(alias="isDeleted")
class TagsResponseDto(BaseModel):
tags: list[str]
class CreateUserResponseDto(UserResponseDto):
pass
class UpdateUserResponseDto(UserResponseDto):
pass
class DisableUserResponseDto(UserResponseDto):
pass
class EnableUserResponseDto(UserResponseDto):
pass
class ResetUserTrafficResponseDto(UserResponseDto):
pass
class RevokeUserSubscriptionResponseDto(UserResponseDto):
pass
class ActivateAllInboundsResponseDto(UserResponseDto):
pass
class GetUserByUuidResponseDto(UserResponseDto):
pass
class GetUserByShortUuidResponseDto(UserResponseDto):
pass
class GetUserByUsernameResponseDto(UserResponseDto):
pass
@property
def used_traffic_bytes(self) -> float:
"""Backward compatibility property"""
return self.user_traffic.used_traffic_bytes
@property
def lifetime_used_traffic_bytes(self) -> float:
"""Backward compatibility property"""
return self.user_traffic.lifetime_used_traffic_bytes
@property
def online_at(self) -> Optional[datetime]:
"""Backward compatibility property"""
return self.user_traffic.online_at
@property
def first_connected(self) -> Optional[datetime]:
"""Backward compatibility property"""
return self.user_traffic.first_connected_at
@property
def last_connected_node_uuid(self) -> Optional[UUID]:
"""Backward compatibility property"""
return self.user_traffic.last_connected_node_uuid
@property
def happ(self) -> HappCrypto:
"""Generate Happ Crypto Link"""
crypto_link = create_happ_crypto_link(self.subscription_url)
return HappCrypto(cryptoLink=crypto_link)
class RevokeUserRequestDto(BaseModel):
short_uuid: str | None = Field(
"""Request DTO for revoking user subscription"""
short_uuid: Optional[str] = Field(
None,
serialization_alias="shortUuid",
description="Optional. If not provided, a new short UUID will be generated by Remnawave. Please note that it is strongly recommended to allow Remnawave to generate the short UUID.",
description="Optional. If not provided, a new short UUID will be generated by Remnawave.",
min_length=6,
max_length=48,
pattern=r"^[a-zA-Z0-9_-]+$",
@ -226,6 +192,7 @@ class RevokeUserRequestDto(BaseModel):
class SubscriptionRequestRecord(BaseModel):
"""Subscription request history record"""
id: int
user_uuid: UUID = Field(alias="userUuid")
request_at: datetime = Field(alias="requestAt")
@ -234,9 +201,124 @@ class SubscriptionRequestRecord(BaseModel):
class SubscriptionRequestsResponseData(BaseModel):
"""Subscription requests response data"""
total: int
records: List[SubscriptionRequestRecord]
class CreateUserResponseDto(UserResponseDto):
"""Response for create user"""
pass
class UpdateUserResponseDto(UserResponseDto):
"""Response for update user"""
pass
class GetUserByUuidResponseDto(UserResponseDto):
"""Response for get user by UUID"""
pass
class GetUserByShortUuidResponseDto(UserResponseDto):
"""Response for get user by short UUID"""
pass
class GetUserByUsernameResponseDto(UserResponseDto):
"""Response for get user by username"""
pass
class GetUserByIdResponseDto(UserResponseDto):
"""Response for get user by ID"""
pass
class DisableUserResponseDto(UserResponseDto):
"""Response for disable user"""
pass
class EnableUserResponseDto(UserResponseDto):
"""Response for enable user"""
pass
class ResetUserTrafficResponseDto(UserResponseDto):
"""Response for reset user traffic"""
pass
class RevokeUserSubscriptionResponseDto(UserResponseDto):
"""Response for revoke user subscription"""
pass
class ActivateAllInboundsResponseDto(UserResponseDto):
"""Response for activate all inbounds"""
pass
class UsersResponseDto(BaseModel):
"""Users collection response"""
users: list[UserResponseDto]
total: int
class GetAllUsersResponseDto(UsersResponseDto):
"""Response for get all users"""
pass
class TagsResponseDto(BaseModel):
"""Tags collection response"""
tags: list[str]
class GetAllTagsResponseDto(TagsResponseDto):
"""Response for get all tags"""
pass
class GetSubscriptionRequestsResponseDto(SubscriptionRequestsResponseData):
pass
"""Response for get subscription requests"""
pass
class GetUserSubscriptionRequestHistoryResponseDto(SubscriptionRequestsResponseData):
"""Response for get user subscription request history"""
pass
class DeleteUserResponseDto(BaseModel):
"""Response for delete user"""
is_deleted: bool = Field(alias="isDeleted")
class EmailUserResponseDto(RootModel[list[UserResponseDto]]):
"""Response for get users by email"""
def __iter__(self):
return iter(self.root)
def __getitem__(self, item):
return self.root[item]
class TagUserResponseDto(RootModel[list[UserResponseDto]]):
"""Response for get users by tag"""
def __iter__(self):
return iter(self.root)
def __getitem__(self, item):
return self.root[item]
class TelegramUserResponseDto(RootModel[list[UserResponseDto]]):
"""Response for get users by telegram ID"""
def __iter__(self):
return iter(self.root)
def __getitem__(self, item):
return self.root[item]

View file

@ -1,97 +1,203 @@
from datetime import datetime
from typing import Annotated, List, Literal, Optional
from typing import Annotated, List, Optional
from uuid import UUID
from pydantic import BaseModel, EmailStr, Field, constr
from pydantic import BaseModel, Field
from remnawave.enums import UserStatus
from remnawave.enums.users import TrafficLimitStrategy
from remnawave.enums import UserStatus, TrafficLimitStrategy
class BulkUpdateUsersInternalSquadsRequestDto(BaseModel):
uuids: List[UUID]
active_internal_squads: List[UUID] = Field(serialization_alias="activeInternalSquads")
# Type alias for tag validation
TagStr = Annotated[
str,
Field(min_length=1, max_length=16, pattern=r"^[A-Z0-9_]+$")
]
# Request DTOs
class BulkDeleteUsersByStatusRequestDto(BaseModel):
"""Request to delete users by status"""
status: UserStatus = Field(default=UserStatus.ACTIVE)
class BulkDeleteUsersRequestDto(BaseModel):
"""Request to delete users by UUIDs"""
uuids: List[UUID] = Field(min_length=1, max_length=500)
class BulkRevokeUsersSubscriptionRequestDto(BaseModel):
"""Request to revoke users subscription"""
uuids: List[UUID] = Field(min_length=1, max_length=500)
class BulkResetTrafficUsersRequestDto(BaseModel):
"""Request to reset traffic for users"""
uuids: List[UUID] = Field(min_length=1, max_length=500)
class UpdateUserFields(BaseModel):
"""Fields to update for users"""
status: Optional[UserStatus] = None
traffic_limit_bytes: Optional[int] = Field(
None, serialization_alias="trafficLimitBytes", strict=True, ge=0
None,
serialization_alias="trafficLimitBytes",
ge=0,
description="Traffic limit in bytes. 0 - unlimited"
)
traffic_limit_strategy: Optional[TrafficLimitStrategy] = Field(
None, serialization_alias="trafficLimitStrategy"
None,
serialization_alias="trafficLimitStrategy",
description="Traffic limit reset strategy"
)
expire_at: Optional[datetime] = Field(
None,
serialization_alias="expireAt",
description="Expiration date: 2025-01-17T15:38:45.065Z"
)
expire_at: Optional[datetime] = Field(None, serialization_alias="expireAt")
description: Optional[str] = None
telegram_id: Optional[int] = Field(None, serialization_alias="telegramId")
email: Optional[str] = None
tag: Optional[TagStr] = Field(
None,
description="Tag for user. Must be uppercase, alphanumeric, and can include underscores. Max length 16 characters."
)
telegram_id: Optional[int] = Field(None, serialization_alias="telegramId")
email: Optional[str] = None
hwid_device_limit: Optional[int] = Field(
None, serialization_alias="hwidDeviceLimit", strict=True, ge=0
None,
serialization_alias="hwidDeviceLimit",
ge=0
)
telegram_id: Optional[int] = Field(
None, serialization_alias="telegramId"
external_squad_uuid: Optional[UUID] = Field(
None,
serialization_alias="externalSquadUuid",
description="Optional. External squad UUID."
)
class BulkUpdateUsersRequestDto(BaseModel):
"""Request to bulk update users"""
uuids: List[UUID] = Field(min_length=1, max_length=500)
fields: UpdateUserFields
class BulkUpdateUsersSquadsRequestDto(BaseModel):
"""Request to update users internal squads"""
uuids: List[UUID] = Field(min_length=1, max_length=500)
active_internal_squads: List[UUID] = Field(serialization_alias="activeInternalSquads")
class BulkExtendExpirationDateRequestDto(BaseModel):
"""Request to extend expiration date for selected users"""
uuids: List[UUID] = Field(min_length=1, max_length=500)
extend_days: int = Field(
serialization_alias="extendDays",
ge=1,
le=9999
)
class BulkAllUpdateUsersRequestDto(BaseModel):
status: Optional[Literal["ACTIVE", "DISABLED", "LIMITED", "EXPIRED"]] = Field(
"ACTIVE"
)
"""Request to update all users"""
status: Optional[UserStatus] = Field(default=UserStatus.ACTIVE)
traffic_limit_bytes: Optional[int] = Field(
None,
serialization_alias="trafficLimitBytes",
ge=0,
description="Traffic limit in bytes. 0 - unlimited"
)
traffic_limit_strategy: Optional[Literal["NO_RESET", "DAY", "WEEK", "MONTH"]] = Field(
traffic_limit_strategy: Optional[TrafficLimitStrategy] = Field(
None,
serialization_alias="trafficLimitStrategy",
description="Traffic limit reset strategy"
)
expire_at: Optional[datetime] = Field(
None,
serialization_alias="expireAt",
description="Expiration date"
description="Expiration date: 2025-01-17T15:38:45.065Z"
)
description: Optional[str] = None
telegram_id: Optional[int] = Field(
None,
serialization_alias="telegramId"
)
hwid_device_limit: Optional[int] = Field(
None, serialization_alias="hwidDeviceLimit", strict=True, ge=0
)
telegram_id: Optional[int] = Field(None, serialization_alias="telegramId")
email: Optional[str] = None
tag: Optional[TagStr] = Field(
None,
description="Tag for user. Must be uppercase, alphanumeric, and can include underscores. Max length 16 characters."
)
hwid_device_limit: Optional[int] = Field(
None,
serialization_alias="hwidDeviceLimit",
ge=0
)
class BulkResponseDto(BaseModel):
class BulkAllExtendExpirationDateRequestDto(BaseModel):
"""Request to extend expiration date for all users"""
extend_days: int = Field(
serialization_alias="extendDays",
ge=1
)
# Base Response DTOs (без обертки response)
class BulkResponseData(BaseModel):
"""Common bulk response with affected rows"""
affected_rows: int = Field(alias="affectedRows")
class BulkAllResetTrafficUsersResponseDto(BaseModel):
class BulkEventResponseData(BaseModel):
"""Common bulk response with event sent flag"""
event_sent: bool = Field(alias="eventSent")
class BulkAllUpdateUsersResponseDto(BaseModel):
event_sent: bool = Field(alias="eventSent")
# Response DTOs - наследуются от базовых
class BulkDeleteUsersByStatusResponseDto(BulkResponseData):
"""Response for bulk delete by status"""
pass
class BulkDeleteUsersResponseDto(BulkResponseData):
"""Response for bulk delete users"""
pass
class BulkRevokeUsersSubscriptionResponseDto(BulkResponseData):
"""Response for bulk revoke subscription"""
pass
class BulkResetTrafficUsersResponseDto(BulkResponseData):
"""Response for bulk reset traffic"""
pass
class BulkUpdateUsersResponseDto(BulkResponseData):
"""Response for bulk update users"""
pass
class BulkUpdateUsersSquadsResponseDto(BulkResponseData):
"""Response for bulk update squads"""
pass
class BulkExtendExpirationDateResponseDto(BulkResponseData):
"""Response for bulk extend expiration date"""
pass
class BulkAllUpdateUsersResponseDto(BulkEventResponseData):
"""Response for bulk update all users"""
pass
class BulkAllResetTrafficUsersResponseDto(BulkEventResponseData):
"""Response for bulk reset all users traffic"""
pass
class BulkAllExtendExpirationDateResponseDto(BulkEventResponseData):
"""Response for bulk extend all users expiration date"""
pass
# Legacy compatibility
BulkResponseDto = BulkResponseData
BulkUpdateUsersInternalSquadsRequestDto = BulkUpdateUsersSquadsRequestDto

View file

@ -0,0 +1,57 @@
import base64
from typing import Literal
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
HAPP_PUBLIC_KEY_V3 = b"""
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAlBetA0wjbaj+h7oJ/d/h
pNrXvAcuhOdFGEFcfCxSWyLzWk4SAQ05gtaEGZyetTax2uqagi9HT6lapUSUe2S8
nMLJf5K+LEs9TYrhhBdx/B0BGahA+lPJa7nUwp7WfUmSF4hir+xka5ApHjzkAQn6
cdG6FKtSPgq1rYRPd1jRf2maEHwiP/e/jqdXLPP0SFBjWTMt/joUDgE7v/IGGB0L
Q7mGPAlgmxwUHVqP4bJnZ//5sNLxWMjtYHOYjaV+lixNSfhFM3MdBndjpkmgSfmg
D5uYQYDL29TDk6Eu+xetUEqry8ySPjUbNWdDXCglQWMxDGjaqYXMWgxBA1UKjUBW
wbgr5yKTJ7mTqhlYEC9D5V/LOnKd6pTSvaMxkHXwk8hBWvUNWAxzAf5JZ7EVE3jt
0j682+/hnmL/hymUE44yMG1gCcWvSpB3BTlKoMnl4yrTakmdkbASeFRkN3iMRewa
IenvMhzJh1fq7xwX94otdd5eLB2vRFavrnhOcN2JJAkKTnx9dwQwFpGEkg+8U613
+Tfm/f82l56fFeoFN98dD2mUFLFZoeJ5CG81ZeXrH83niI0joX7rtoAZIPWzq3Y1
Zb/Zq+kK2hSIhphY172Uvs8X2Qp2ac9UoTPM71tURsA9IvPNvUwSIo/aKlX5KE3I
VE0tje7twWXL5Gb1sfcXRzsCAwEAAQ==
-----END PUBLIC KEY-----
"""
HAPP_PUBLIC_KEY_V4 = b"""
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA3UZ0M3L4K+WjM3vkbQnz
ozHg/cRbEXvQ6i4A8RVN4OM3rK9kU01FdjyoIgywve8OEKsFnVwERZAQZ1Trv60B
hmaM76QQEE+EUlIOL9EpwKWGtTL5lYC1sT9XJMNP3/CI0gP5wwQI88cY/xedpOEB
W72EmOOShHUm/b/3m+HPmqwc4ugKj5zWV5SyiT829aFA5DxSjmIIFBAms7DafmSq
LFTYIQL5cShDY2u+/sqyAw9yZIOoqW2TFIgIHhLPWek/ocDU7zyOrlu1E0SmcQQb
LFqHq02fsnH6IcqTv3N5Adb/CkZDDQ6HvQVBmqbKZKf7ZdXkqsc/Zw27xhG7OfXC
tUmWsiL7zA+KoTd3avyOh93Q9ju4UQsHthL3Gs4vECYOCS9dsXXSHEY/1ngU/hjO
WFF8QEE/rYV6nA4PTyUvo5RsctSQL/9DJX7XNh3zngvif8LsCN2MPvx6X+zLouBX
zgBkQ9DFfZAGLWf9TR7KVjZC/3NsuUCDoAOcpmN8pENBbeB0puiKMMWSvll36+2M
YR1Xs0MgT8Y9TwhE2+TnnTJOhzmHi/BxiUlY/w2E0s4ax9GHAmX0wyF4zeV7kDkc
vHuEdc0d7vDmdw0oqCqWj0Xwq86HfORu6tm1A8uRATjb4SzjTKclKuoElVAVa5Jo
oh/uZMozC65SmDw+N5p6Su8CAwEAAQ==
-----END PUBLIC KEY-----
"""
def create_happ_crypto_link(content: str, method: Literal["v3", "v4"] = "v4") -> str:
try:
happ_methods = {
"v3": HAPP_PUBLIC_KEY_V3,
"v4": HAPP_PUBLIC_KEY_V4,
}
public_key = serialization.load_pem_public_key(happ_methods[method.lower()])
encrypted = public_key.encrypt(
content.encode("utf-8"), padding.PKCS1v15() # RSA_PKCS1_PADDING
)
return "happ://crypt{" + method.lower().replace("v", "") + "/" + base64.b64encode(encrypted).decode()
except Exception:
return ""

View file

@ -234,8 +234,8 @@ class TestHostsAdvanced:
host="example.org",
allow_insecure=False,
is_disabled=False,
muxParams='{"enabled": true, "concurrency": 8}',
sockopt_params='{"mark": 255}',
mux_params={"enabled": True, "concurrency": 8},
sockopt_params={"mark": 255},
tag="ADVANCED",
is_hidden=False,
override_sni_from_address=True,

View file

@ -23,19 +23,3 @@ async def test_nodes_usage_history(remnawave) -> None:
assert isinstance(nodes_usage, GetNodesUsageByRangeResponseDto)
# Response should be a list now (RootModel)
assert isinstance(nodes_usage.root, list)
@pytest.mark.asyncio
async def test_nodes_user_usage_history(remnawave) -> None:
start_date = (datetime.now() - timedelta(days=7)).isoformat()
end_date = datetime.now().isoformat()
node_user_usage = await remnawave.nodes_user_usage_history.get_node_user_usage_by_range(
uuid=REMNAWAVE_USER_UUID,
start=start_date,
end=end_date
)
assert isinstance(node_user_usage, GetNodeUserUsageByRangeResponseDto)

View file

@ -19,6 +19,7 @@ from remnawave.models import (
RevokeUserRequestDto,
GetSubscriptionRequestsResponseDto
)
from remnawave.models.users import GetUserSubscriptionRequestHistoryResponseDto
from tests.utils import generate_email, generate_random_string
@ -110,7 +111,7 @@ class TestUsersFetch:
@pytest.mark.asyncio
async def test_get_all_users(self, remnawave):
all_users = await remnawave.users.get_all_users_v2()
all_users = await remnawave.users.get_all_users()
assert isinstance(all_users, UsersResponseDto)
@pytest.mark.asyncio
@ -173,12 +174,10 @@ class TestUsersFetch:
"""Test fetching user subscription request history"""
string_uuid = str(test_user.uuid)
try:
subscription_requests = await remnawave.users.get_subscription_requests(uuid=string_uuid)
assert isinstance(subscription_requests, GetSubscriptionRequestsResponseDto)
subscription_requests = await remnawave.users.get_user_subscription_request_history(uuid=string_uuid)
assert isinstance(subscription_requests, GetUserSubscriptionRequestHistoryResponseDto)
assert hasattr(subscription_requests, 'total')
assert hasattr(subscription_requests, 'records')
# Даже если записей нет, модель должна быть правильно сформирована
# с пустым списком records и total=0
except ApiError as e:
# Этот блок должен срабатывать только если API вернуло ошибку
# (404, 403 и т.д.), но не когда просто нет записей

View file

@ -4,7 +4,7 @@ from typing import List
import pytest
import pytz
from remnawave.models import BulkResponseDto, UpdateUserFields
from remnawave.models import BulkResponseDto, UpdateUserFields, BulkUpdateUsersRequestDto
from tests.conftest import REMNAWAVE_USER_UUID
@ -12,14 +12,15 @@ from tests.conftest import REMNAWAVE_USER_UUID
async def test_users_bulk_actions(remnawave):
expire_at = datetime.now(tz=pytz.utc) + timedelta(days=14)
description = "TEST_DESCRIPTION"
uuids: List[str] = [REMNAWAVE_USER_UUID]
bulk_update_users = await remnawave.users_bulk_actions.bulk_update_users(
uuids=uuids,
fields=UpdateUserFields(
description=description,
expire_at=expire_at,
body=BulkUpdateUsersRequestDto(
uuids=[REMNAWAVE_USER_UUID],
fields=UpdateUserFields(
expire_at=expire_at,
description=description,
),
),
)
assert isinstance(bulk_update_users, BulkResponseDto)
assert bulk_update_users.affected_rows == len(uuids)
assert bulk_update_users.affected_rows > 0