diff --git a/README.md b/README.md index 4dc0468..cb28358 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/poetry.lock b/poetry.lock index e029d68..df7809a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index e34b8a6..0faaf8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ diff --git a/remnawave/controllers/users.py b/remnawave/controllers/users.py index 745e0f6..b0417d2 100644 --- a/remnawave/controllers/users.py +++ b/remnawave/controllers/users.py @@ -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""" ... \ No newline at end of file diff --git a/remnawave/controllers/users_bulk_actions.py b/remnawave/controllers/users_bulk_actions.py index c4426fb..3b3b205 100644 --- a/remnawave/controllers/users_bulk_actions.py +++ b/remnawave/controllers/users_bulk_actions.py @@ -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""" ... \ No newline at end of file diff --git a/remnawave/models/__init__.py b/remnawave/models/__init__.py index b428c12..844b1d0 100644 --- a/remnawave/models/__init__.py +++ b/remnawave/models/__init__.py @@ -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", diff --git a/remnawave/models/external_squads.py b/remnawave/models/external_squads.py index 5b327e9..7e3b1e5 100644 --- a/remnawave/models/external_squads.py +++ b/remnawave/models/external_squads.py @@ -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): diff --git a/remnawave/models/hosts.py b/remnawave/models/hosts.py index 6fb6ab7..1ceb4d7 100644 --- a/remnawave/models/hosts.py +++ b/remnawave/models/hosts.py @@ -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] \ No newline at end of file + +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 \ No newline at end of file diff --git a/remnawave/models/nodes.py b/remnawave/models/nodes.py index 94720ad..871df82 100644 --- a/remnawave/models/nodes.py +++ b/remnawave/models/nodes.py @@ -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") diff --git a/remnawave/models/nodes_usage_history.py b/remnawave/models/nodes_usage_history.py index 69111be..cfa55b2 100644 --- a/remnawave/models/nodes_usage_history.py +++ b/remnawave/models/nodes_usage_history.py @@ -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") diff --git a/remnawave/models/remnawave_settings.py b/remnawave/models/remnawave_settings.py index c73cbf3..5f04010 100644 --- a/remnawave/models/remnawave_settings.py +++ b/remnawave/models/remnawave_settings.py @@ -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 \ No newline at end of file + pass \ No newline at end of file diff --git a/remnawave/models/subscription.py b/remnawave/models/subscription.py index f337634..28a4686 100644 --- a/remnawave/models/subscription.py +++ b/remnawave/models/subscription.py @@ -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): diff --git a/remnawave/models/subscriptions_settings.py b/remnawave/models/subscriptions_settings.py index 52ff381..41a69ae 100644 --- a/remnawave/models/subscriptions_settings.py +++ b/remnawave/models/subscriptions_settings.py @@ -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") \ No newline at end of file + 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") \ No newline at end of file diff --git a/remnawave/models/system.py b/remnawave/models/system.py index b8c7159..7e085ad 100644 --- a/remnawave/models/system.py +++ b/remnawave/models/system.py @@ -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 \ No newline at end of file +class DebugSrrMatcherResponseDto(DebugSrrMatcherData): + pass \ No newline at end of file diff --git a/remnawave/models/users.py b/remnawave/models/users.py index 7cd03f4..f817d58 100644 --- a/remnawave/models/users.py +++ b/remnawave/models/users.py @@ -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 \ No newline at end of file + """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] \ No newline at end of file diff --git a/remnawave/models/users_bulk_actions.py b/remnawave/models/users_bulk_actions.py index 30aad18..c5275c1 100644 --- a/remnawave/models/users_bulk_actions.py +++ b/remnawave/models/users_bulk_actions.py @@ -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 \ No newline at end of file diff --git a/remnawave/utils/happ_crypt.py b/remnawave/utils/happ_crypt.py new file mode 100644 index 0000000..a70babc --- /dev/null +++ b/remnawave/utils/happ_crypt.py @@ -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 "" diff --git a/tests/test_hosts.py b/tests/test_hosts.py index 24504dd..0878454 100644 --- a/tests/test_hosts.py +++ b/tests/test_hosts.py @@ -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, diff --git a/tests/test_nodes_usage_history.py b/tests/test_nodes_usage_history.py index 766a959..7a123fa 100644 --- a/tests/test_nodes_usage_history.py +++ b/tests/test_nodes_usage_history.py @@ -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) - - diff --git a/tests/test_users.py b/tests/test_users.py index 9eba5a9..18ffb76 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -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 и т.д.), но не когда просто нет записей diff --git a/tests/test_users_bulk_actions.py b/tests/test_users_bulk_actions.py index 3cf12e1..245a7b0 100644 --- a/tests/test_users_bulk_actions.py +++ b/tests/test_users_bulk_actions.py @@ -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