From 9e2822774a289d16969b73adaeeded51b3649bab Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 28 Oct 2025 00:44:15 +0100 Subject: [PATCH] feat: Enhance subscription and external squad management - Updated NodeUsageDto to include node_uuid and changed date to datetime. - Changed total fields in subscription and subscription request history models from float to int. - Introduced response rules and conditions for subscription settings, including new models for response modifications. - Added external squads management with CRUD operations and associated models. - Implemented passkey management with registration and verification endpoints. - Enhanced snippets management with full CRUD operations and validation. - Added OAuth2 provider enum for better authentication handling. --- README.md | 3 +- pyproject.toml | 4 +- remnawave/__init__.py | 10 +- remnawave/controllers/__init__.py | 9 ++ remnawave/controllers/auth.py | 38 +++++ remnawave/controllers/config_profiles.py | 9 ++ remnawave/controllers/external_squads.py | 73 +++++++++ remnawave/controllers/nodes.py | 6 +- remnawave/controllers/passkeys.py | 45 ++++++ remnawave/controllers/remnawave_settings.py | 25 +++ remnawave/controllers/snippets.py | 45 ++++++ .../controllers/subscriptions_template.py | 55 +++++-- remnawave/controllers/system.py | 26 ++- remnawave/controllers/users_bulk_actions.py | 12 ++ remnawave/enums/__init__.py | 13 ++ remnawave/enums/auth.py | 7 + remnawave/enums/subscriptions_settings.py | 40 +++++ remnawave/models/__init__.py | 152 +++++++++++++++++- remnawave/models/auth.py | 47 +++++- remnawave/models/bandwidthstats.py | 6 +- remnawave/models/config_profiles.py | 6 +- remnawave/models/external_squads.py | 102 ++++++++++++ remnawave/models/hosts.py | 137 ++++++++-------- remnawave/models/inbounds.py | 4 +- remnawave/models/infra_billing.py | 2 +- remnawave/models/nodes.py | 73 +++++---- remnawave/models/nodes_usage_history.py | 11 +- remnawave/models/passkeys.py | 46 ++++++ remnawave/models/remnawave_settings.py | 88 ++++++++++ remnawave/models/snippets.py | 56 +++++++ remnawave/models/subscription.py | 2 +- .../models/subscription_request_history.py | 2 +- remnawave/models/subscriptions_settings.py | 80 +++++++-- remnawave/models/subscriptions_template.py | 53 +++++- remnawave/models/system.py | 63 ++++++-- remnawave/models/users.py | 6 +- remnawave/models/users_bulk_actions.py | 11 ++ tests/test_snippets.py | 77 +++++++++ tests/test_subscriptions_template.py | 92 +++++++++-- 39 files changed, 1355 insertions(+), 181 deletions(-) create mode 100644 remnawave/controllers/external_squads.py create mode 100644 remnawave/controllers/passkeys.py create mode 100644 remnawave/controllers/remnawave_settings.py create mode 100644 remnawave/controllers/snippets.py create mode 100644 remnawave/enums/auth.py create mode 100644 remnawave/enums/subscriptions_settings.py create mode 100644 remnawave/models/external_squads.py create mode 100644 remnawave/models/passkeys.py create mode 100644 remnawave/models/remnawave_settings.py create mode 100644 remnawave/models/snippets.py create mode 100644 tests/test_snippets.py diff --git a/README.md b/README.md index 588cc1e..786f991 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.1.19 | >=2.1.19 | +| 2.2.13 | >=2.2.0 | +| 2.1.19 | >=2.1.19, <2.2.0 | | 2.1.18 | >=2.1.18 | | 2.1.17 | >=2.1.16, <=2.1.17 | | 2.1.16 | >=2.1.16 | diff --git a/pyproject.toml b/pyproject.toml index e06ef9b..d595039 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "remnawave" -version = "2.1.19.post.1" -description = "A Python SDK for interacting with the Remnawave API v2.1.19." +version = "2.2.3" +description = "A Python SDK for interacting with the Remnawave API v2.2.3." authors = [ {name = "Artem",email = "dev@forestsnet.com"} ] diff --git a/remnawave/__init__.py b/remnawave/__init__.py index 6f02b8d..95d38fc 100644 --- a/remnawave/__init__.py +++ b/remnawave/__init__.py @@ -29,7 +29,11 @@ from remnawave.controllers import ( UsersStatsController, WebhookUtility, XrayConfigController, - SubscriptionRequestHistoryController + SubscriptionRequestHistoryController, + PasskeysController, + ExternalSquadsController, + SnippetsController, + RemnawaveSettingsController, # WebhookUtility is not a controller, but it's included in the controllers module for convenience ) @@ -93,6 +97,10 @@ class RemnawaveSDK: self.users_stats = UsersStatsController(self._client) self.webhook_utility = WebhookUtility() self.xray_config = XrayConfigController(self._client) + self.passkeys = PasskeysController(self._client) + self.external_squads = ExternalSquadsController(self._client) + self.snippets = SnippetsController(self._client) + self.remnawave_settings = RemnawaveSettingsController(self._client) def _validate_params(self) -> None: if self._client is None: diff --git a/remnawave/controllers/__init__.py b/remnawave/controllers/__init__.py index e1d4413..c425c80 100644 --- a/remnawave/controllers/__init__.py +++ b/remnawave/controllers/__init__.py @@ -23,6 +23,11 @@ from .users_stats import UsersStatsController from .webhooks import WebhookUtility from .xray_config import XrayConfigController from .subscriptions_request import SubscriptionRequestHistoryController +from .passkeys import PasskeysController +from .external_squads import ExternalSquadsController +from .snippets import SnippetsController +from .remnawave_settings import RemnawaveSettingsController + __all__ = [ "APITokensManagementController", @@ -51,4 +56,8 @@ __all__ = [ "WebhookUtility", "XrayConfigController", "SubscriptionRequestHistoryController", + "PasskeysController", + "ExternalSquadsController", + "SnippetsController", + "RemnawaveSettingsController", ] diff --git a/remnawave/controllers/auth.py b/remnawave/controllers/auth.py index 7d1e44f..441dd95 100644 --- a/remnawave/controllers/auth.py +++ b/remnawave/controllers/auth.py @@ -6,6 +6,13 @@ from remnawave.models import ( GetStatusResponseDto, LoginRequestDto, LoginResponseDto, + OAuth2AuthorizeRequestDto, + OAuth2AuthorizeResponseDto, + OAuth2CallbackRequestDto, + OAuth2CallbackResponseDto, + VerifyPasskeyAuthenticationRequestDto, + VerifyPasskeyAuthenticationResponseDto, + GetPasskeyAuthenticationOptionsResponseDto, RegisterRequestDto, RegisterResponseDto, TelegramCallbackRequestDto, @@ -44,4 +51,35 @@ class AuthController(BaseController): body: Annotated[TelegramCallbackRequestDto, PydanticBody()], ) -> TelegramCallbackResponseDto: """OAuth2 Telegram callback""" + ... + + @post("/auth/oauth2/authorize", response_class=OAuth2AuthorizeResponseDto) + async def oauth2_authorize( + self, + body: Annotated[OAuth2AuthorizeRequestDto, PydanticBody()], + ) -> OAuth2AuthorizeResponseDto: + """Initiate OAuth2 authorization""" + ... + + @post("/auth/oauth2/callback", response_class=OAuth2CallbackResponseDto) + async def oauth2_callback( + self, + body: Annotated[OAuth2CallbackRequestDto, PydanticBody()], + ) -> OAuth2CallbackResponseDto: + """Callback from OAuth2""" + ... + + @get("/auth/passkey/authentication/options", response_class=GetPasskeyAuthenticationOptionsResponseDto) + async def passkey_authentication_options( + self, + ) -> GetPasskeyAuthenticationOptionsResponseDto: + """Get the authentication options for passkey""" + ... + + @post("/auth/passkey/authentication/verify", response_class=VerifyPasskeyAuthenticationResponseDto) + async def passkey_authentication_verify( + self, + body: Annotated[VerifyPasskeyAuthenticationRequestDto, PydanticBody()], + ) -> VerifyPasskeyAuthenticationResponseDto: + """Verify the authentication for passkey""" ... \ No newline at end of file diff --git a/remnawave/controllers/config_profiles.py b/remnawave/controllers/config_profiles.py index f798447..0dfb441 100644 --- a/remnawave/controllers/config_profiles.py +++ b/remnawave/controllers/config_profiles.py @@ -67,3 +67,12 @@ class ConfigProfilesController(BaseController): ) -> DeleteConfigProfileResponseDto: """Delete config profile""" ... + + # Get computed config profile by uuid​ + @get("/config-profiles/{uuid}/computed-config", response_class=GetConfigProfileByUuidResponseDto) + async def get_computed_config_profile_by_uuid( + self, + uuid: Annotated[str, Path(description="UUID of the config profile")], + ) -> GetConfigProfileByUuidResponseDto: + """Get computed config profile by uuid""" + ... \ No newline at end of file diff --git a/remnawave/controllers/external_squads.py b/remnawave/controllers/external_squads.py new file mode 100644 index 0000000..0b62504 --- /dev/null +++ b/remnawave/controllers/external_squads.py @@ -0,0 +1,73 @@ +from typing import Annotated + +from rapid_api_client.annotations import PydanticBody + +from remnawave.models import ( + AddUsersToExternalSquadResponseDto, + CreateExternalSquadRequestDto, + CreateExternalSquadResponseDto, + DeleteExternalSquadResponseDto, + GetExternalSquadByUuidResponseDto, + GetExternalSquadsResponseDto, + RemoveUsersFromExternalSquadResponseDto, + UpdateExternalSquadRequestDto, + UpdateExternalSquadResponseDto, +) +from remnawave.rapid import BaseController, delete, get, patch, post + + +class ExternalSquadsController(BaseController): + @get("/external-squads", response_class=GetExternalSquadsResponseDto) + async def get_external_squads( + self, + ) -> GetExternalSquadsResponseDto: + """Get all external squads""" + ... + + @post("/external-squads", response_class=CreateExternalSquadResponseDto) + async def create_external_squad( + self, + body: Annotated[CreateExternalSquadRequestDto, PydanticBody()], + ) -> CreateExternalSquadResponseDto: + """Create external squad""" + ... + + @patch("/external-squads", response_class=UpdateExternalSquadResponseDto) + async def update_external_squad( + self, + body: Annotated[UpdateExternalSquadRequestDto, PydanticBody()], + ) -> UpdateExternalSquadResponseDto: + """Update external squad""" + ... + + @get("/external-squads/{uuid}", response_class=GetExternalSquadByUuidResponseDto) + async def get_external_squad_by_uuid( + self, + uuid: str, + ) -> GetExternalSquadByUuidResponseDto: + """Get external squad by uuid""" + ... + + @delete("/external-squads/{uuid}", response_class=DeleteExternalSquadResponseDto) + async def delete_external_squad( + self, + uuid: str, + ) -> DeleteExternalSquadResponseDto: + """Delete external squad""" + ... + + @post("/external-squads/{uuid}/bulk-actions/add-users", response_class=AddUsersToExternalSquadResponseDto) + async def add_users_to_external_squad( + self, + uuid: str, + ) -> AddUsersToExternalSquadResponseDto: + """Add all users to external squad""" + ... + + @delete("/external-squads/{uuid}/bulk-actions/remove-users", response_class=RemoveUsersFromExternalSquadResponseDto) + async def remove_users_from_external_squad( + self, + uuid: str, + ) -> RemoveUsersFromExternalSquadResponseDto: + """Delete users from external squad""" + ... \ No newline at end of file diff --git a/remnawave/controllers/nodes.py b/remnawave/controllers/nodes.py index 53de2d4..d19889e 100644 --- a/remnawave/controllers/nodes.py +++ b/remnawave/controllers/nodes.py @@ -17,7 +17,7 @@ from remnawave.models import ( RestartNodeResponseDto, UpdateNodeRequestDto, UpdateNodeResponseDto, - RestartAllNodesRequestDto, + RestartAllNodesRequestBodyDto, ) from remnawave.rapid import BaseController, delete, get, patch, post @@ -89,7 +89,7 @@ class NodesController(BaseController): @post("/nodes/actions/restart-all", response_class=RestartAllNodesResponseDto) async def restart_all_nodes( self, - body: Annotated[RestartAllNodesRequestDto, PydanticBody()], + body: Annotated[RestartAllNodesRequestBodyDto, PydanticBody()], ) -> RestartAllNodesResponseDto: """Restart All Nodes""" ... @@ -100,4 +100,4 @@ class NodesController(BaseController): body: Annotated[ReorderNodeRequestDto, PydanticBody()], ) -> ReorderNodeResponseDto: """Reorder Nodes""" - ... + ... \ No newline at end of file diff --git a/remnawave/controllers/passkeys.py b/remnawave/controllers/passkeys.py new file mode 100644 index 0000000..55e53db --- /dev/null +++ b/remnawave/controllers/passkeys.py @@ -0,0 +1,45 @@ +from typing import Annotated + +from rapid_api_client.annotations import PydanticBody + +from remnawave.models import ( + DeletePasskeyRequestDto, + DeletePasskeyResponseDto, + GetAllPasskeysResponseDto, + GetPasskeyRegistrationOptionsResponseDto, + VerifyPasskeyRegistrationRequestDto, + VerifyPasskeyRegistrationResponseDto, +) +from remnawave.rapid import BaseController, delete, get, post + + +class PasskeysController(BaseController): + @get("/passkeys/registration/options", response_class=GetPasskeyRegistrationOptionsResponseDto) + async def passkey_registration_options( + self, + ) -> GetPasskeyRegistrationOptionsResponseDto: + """Get registration options for passkey""" + ... + + @post("/passkeys/registration/verify", response_class=VerifyPasskeyRegistrationResponseDto) + async def passkey_registration_verify( + self, + body: Annotated[VerifyPasskeyRegistrationRequestDto, PydanticBody()], + ) -> VerifyPasskeyRegistrationResponseDto: + """Verify registration for passkey""" + ... + + @get("/passkeys", response_class=GetAllPasskeysResponseDto) + async def get_active_passkeys( + self, + ) -> GetAllPasskeysResponseDto: + """Get all passkeys""" + ... + + @delete("/passkeys", response_class=DeletePasskeyResponseDto) + async def delete_passkey( + self, + body: Annotated[DeletePasskeyRequestDto, PydanticBody()], + ) -> DeletePasskeyResponseDto: + """Delete a passkey by ID""" + ... \ No newline at end of file diff --git a/remnawave/controllers/remnawave_settings.py b/remnawave/controllers/remnawave_settings.py new file mode 100644 index 0000000..990dab5 --- /dev/null +++ b/remnawave/controllers/remnawave_settings.py @@ -0,0 +1,25 @@ +from typing import Annotated + +from rapid_api_client.annotations import PydanticBody + +from remnawave.models import ( + GetRemnawaveSettingsResponseDto, + UpdateRemnawaveSettingsRequestDto, + UpdateRemnawaveSettingsResponseDto, +) +from remnawave.rapid import BaseController, get, patch + + +class RemnawaveSettingsController(BaseController): + @get("/remnawave-settings", response_class=GetRemnawaveSettingsResponseDto) + async def get_settings(self) -> GetRemnawaveSettingsResponseDto: + """Get Remnawave settings""" + ... + + @patch("/remnawave-settings", response_class=UpdateRemnawaveSettingsResponseDto) + async def update_settings( + self, + body: Annotated[UpdateRemnawaveSettingsRequestDto, PydanticBody()], + ) -> UpdateRemnawaveSettingsResponseDto: + """Update Remnawave settings""" + ... \ No newline at end of file diff --git a/remnawave/controllers/snippets.py b/remnawave/controllers/snippets.py new file mode 100644 index 0000000..eb457f2 --- /dev/null +++ b/remnawave/controllers/snippets.py @@ -0,0 +1,45 @@ +from typing import Annotated + +from rapid_api_client.annotations import PydanticBody + +from remnawave.models import ( + CreateSnippetRequestDto, + CreateSnippetResponseDto, + DeleteSnippetRequestDto, + DeleteSnippetResponseDto, + GetSnippetsResponseDto, + UpdateSnippetRequestDto, + UpdateSnippetResponseDto, +) +from remnawave.rapid import BaseController, delete, get, post, patch + + +class SnippetsController(BaseController): + @get("/snippets", response_class=GetSnippetsResponseDto) + async def get_snippets(self) -> GetSnippetsResponseDto: + """Get snippets""" + ... + + @post("/snippets", response_class=CreateSnippetResponseDto) + async def create_snippet( + self, + body: Annotated[CreateSnippetRequestDto, PydanticBody()], + ) -> CreateSnippetResponseDto: + """Create snippet""" + ... + + @patch("/snippets", response_class=UpdateSnippetResponseDto) + async def update_snippet( + self, + body: Annotated[UpdateSnippetRequestDto, PydanticBody()], + ) -> UpdateSnippetResponseDto: + """Update snippet""" + ... + + @delete("/snippets", response_class=DeleteSnippetResponseDto) + async def delete_snippet_by_name( + self, + body: Annotated[DeleteSnippetRequestDto, PydanticBody()], + ) -> DeleteSnippetResponseDto: + """Delete snippet""" + ... \ No newline at end of file diff --git a/remnawave/controllers/subscriptions_template.py b/remnawave/controllers/subscriptions_template.py index 3280dd0..72d497f 100644 --- a/remnawave/controllers/subscriptions_template.py +++ b/remnawave/controllers/subscriptions_template.py @@ -3,29 +3,52 @@ from typing import Annotated from rapid_api_client.annotations import Path, PydanticBody from remnawave.enums import TemplateType -from remnawave.models import GetTemplateResponseDto, UpdateTemplateRequestDto, UpdateTemplateResponseDto -from remnawave.rapid import BaseController, get, put +from remnawave.models import ( + CreateSubscriptionTemplateRequestDto, + CreateSubscriptionTemplateResponseDto, + DeleteSubscriptionTemplateResponseDto, + GetTemplateResponseDto, + GetTemplatesResponseDto, + UpdateTemplateRequestDto, + UpdateTemplateResponseDto, +) +from remnawave.rapid import BaseController, delete, get, patch, post class SubscriptionsTemplateController(BaseController): - @get( - "/subscription-templates/{template_type}", - response_class=GetTemplateResponseDto, - ) - async def get_template( - self, - template_type: Annotated[TemplateType, Path(description="Template type")], - ) -> GetTemplateResponseDto: - """Get Template""" + @get("/subscription-templates", response_class=GetTemplatesResponseDto) + async def get_all_templates(self) -> GetTemplatesResponseDto: + """Get all subscription templates (without content)""" ... - @put( - "/subscription-templates", - response_class=UpdateTemplateResponseDto, - ) + @post("/subscription-templates", response_class=CreateSubscriptionTemplateResponseDto) + async def create_template( + self, + body: Annotated[CreateSubscriptionTemplateRequestDto, PydanticBody()], + ) -> CreateSubscriptionTemplateResponseDto: + """Create subscription template""" + ... + + @patch("/subscription-templates", response_class=UpdateTemplateResponseDto) async def update_template( self, body: Annotated[UpdateTemplateRequestDto, PydanticBody()], ) -> UpdateTemplateResponseDto: - """Update Template""" + """Update subscription template""" ... + + @get("/subscription-templates/{uuid}", response_class=GetTemplateResponseDto) + async def get_template_by_uuid( + self, + uuid: Annotated[str, Path(description="Template UUID")], + ) -> GetTemplateResponseDto: + """Get subscription template by uuid""" + ... + + @delete("/subscription-templates/{uuid}", response_class=DeleteSubscriptionTemplateResponseDto) + async def delete_template( + self, + uuid: Annotated[str, Path(description="Template UUID")], + ) -> DeleteSubscriptionTemplateResponseDto: + """Delete subscription template""" + ... \ No newline at end of file diff --git a/remnawave/controllers/system.py b/remnawave/controllers/system.py index 44d685a..f7a8aee 100644 --- a/remnawave/controllers/system.py +++ b/remnawave/controllers/system.py @@ -1,12 +1,18 @@ +from typing import Annotated +from rapid_api_client import PydanticBody from remnawave.models import ( GetBandwidthStatsResponseDto, GetNodesStatisticsResponseDto, GetStatsResponseDto, GetNodesMetricsResponseDto, GetRemnawaveHealthResponseDto, - GetX25519KeyPairResponseDto + GetX25519KeyPairResponseDto, + EncryptHappCryptoLinkRequestDto, + EncryptHappCryptoLinkResponseDto, + DebugSrrMatcherRequestDto, + DebugSrrMatcherResponseDto, ) -from remnawave.rapid import BaseController, get +from remnawave.rapid import BaseController, get, post class SystemController(BaseController): @@ -50,4 +56,20 @@ class SystemController(BaseController): self, ) -> GetX25519KeyPairResponseDto: """Get X25519 Key Pair""" + ... + + @post("/system/tools/happ/encrypt", response_class=EncryptHappCryptoLinkResponseDto) + async def encrypt_happ_crypto_link( + self, + body: Annotated[EncryptHappCryptoLinkRequestDto, PydanticBody()], + ) -> EncryptHappCryptoLinkResponseDto: + """Encrypt Happ Crypto Link""" + ... + + @post("/system/testers/srr-matcher", response_class=DebugSrrMatcherResponseDto) + async def debug_srr_matcher( + self, + body: Annotated[DebugSrrMatcherRequestDto, PydanticBody()], + ) -> DebugSrrMatcherResponseDto: + """Test SRR Matcher""" ... \ No newline at end of file diff --git a/remnawave/controllers/users_bulk_actions.py b/remnawave/controllers/users_bulk_actions.py index a8006da..c4426fb 100644 --- a/remnawave/controllers/users_bulk_actions.py +++ b/remnawave/controllers/users_bulk_actions.py @@ -87,3 +87,15 @@ class UsersBulkActionsController(BaseController): ) -> BulkAllResetTrafficUsersResponseDto: """Bulk Reset All Users Traffic""" ... + + @post( + "/users/bulk/update-squads", + response_class=BulkResponseDto, + ) + async def bulk_update_users_internal_squad( + self, + uuids: Annotated[List[UUID], AttributeBody()], + active_internal_squads: Annotated[List[UUID], AttributeBody(serialization_alias="activeInternalSquads")], + ) -> BulkResponseDto: + """Bulk Update Users External Squad""" + ... \ No newline at end of file diff --git a/remnawave/enums/__init__.py b/remnawave/enums/__init__.py index 4599088..835d0a3 100644 --- a/remnawave/enums/__init__.py +++ b/remnawave/enums/__init__.py @@ -8,7 +8,16 @@ from .users import TrafficLimitStrategy, UserStatus from .webhook import ( TCRMEvents, TErrorsEvents, TNodeEvents, TResetPeriods, TServiceEvents, TUserEvents, TUserHwidDevicesEvents, TUsersStatus ) +from .auth import OAuth2Provider +from .subscriptions_settings import ( + ResponseRuleConditionOperator, + ResponseRuleOperator, + ResponseRuleVersion, + ResponseType, +) + __all__ = [ + "OAuth2Provider", "TrafficLimitStrategy", "UserStatus", "ErrorCode", @@ -17,6 +26,10 @@ __all__ = [ "Fingerprint", "SecurityLayer", "TemplateType", + "ResponseRuleConditionOperator", + "ResponseRuleOperator", + "ResponseRuleVersion", + "ResponseType", # Webhook enums "TNodeEvents", "TUserEvents", diff --git a/remnawave/enums/auth.py b/remnawave/enums/auth.py new file mode 100644 index 0000000..cc18cd6 --- /dev/null +++ b/remnawave/enums/auth.py @@ -0,0 +1,7 @@ +from enum import StrEnum + +class OAuth2Provider(StrEnum): + """OAuth2 Provider enum""" + GITHUB = "github" + POCKETID = "pocketid" + YANDEX = "yandex" \ No newline at end of file diff --git a/remnawave/enums/subscriptions_settings.py b/remnawave/enums/subscriptions_settings.py new file mode 100644 index 0000000..b26ffc5 --- /dev/null +++ b/remnawave/enums/subscriptions_settings.py @@ -0,0 +1,40 @@ +from enum import StrEnum + +class ResponseRuleOperator(StrEnum): + """Response rule logical operators""" + AND = "AND" + OR = "OR" + + +class ResponseRuleConditionOperator(StrEnum): + """Response rule condition operators""" + EQUALS = "EQUALS" + NOT_EQUALS = "NOT_EQUALS" + CONTAINS = "CONTAINS" + NOT_CONTAINS = "NOT_CONTAINS" + STARTS_WITH = "STARTS_WITH" + NOT_STARTS_WITH = "NOT_STARTS_WITH" + ENDS_WITH = "ENDS_WITH" + NOT_ENDS_WITH = "NOT_ENDS_WITH" + REGEX = "REGEX" + NOT_REGEX = "NOT_REGEX" + + +class ResponseType(StrEnum): + """Response types for subscription rules""" + XRAY_JSON = "XRAY_JSON" + XRAY_BASE64 = "XRAY_BASE64" + MIHOMO = "MIHOMO" + STASH = "STASH" + CLASH = "CLASH" + SINGBOX = "SINGBOX" + BROWSER = "BROWSER" + BLOCK = "BLOCK" + STATUS_CODE_404 = "STATUS_CODE_404" + STATUS_CODE_451 = "STATUS_CODE_451" + SOCKET_DROP = "SOCKET_DROP" + + +class ResponseRuleVersion(StrEnum): + """Response rules config version""" + V1 = "1" \ No newline at end of file diff --git a/remnawave/models/__init__.py b/remnawave/models/__init__.py index 3600863..e63ca12 100644 --- a/remnawave/models/__init__.py +++ b/remnawave/models/__init__.py @@ -14,6 +14,13 @@ from .auth import ( TelegramCallbackRequestDto, TelegramCallbackResponseDto, LoginTelegramRequestDto, # Legacy alias + OAuth2AuthorizeRequestDto, + OAuth2AuthorizeResponseDto, + OAuth2CallbackRequestDto, + OAuth2CallbackResponseDto, + VerifyPasskeyAuthenticationRequestDto, + VerifyPasskeyAuthenticationResponseDto, + GetPasskeyAuthenticationOptionsResponseDto, ) from .bandwidthstats import ( GetNodeUserUsageByRangeResponseDto, @@ -36,6 +43,7 @@ from .config_profiles import ( GetConfigProfileByUuidResponseDto, GetInboundsByProfileUuidResponseDto, InboundDto, + NodesProfileDto, UpdateConfigProfileRequestDto, UpdateConfigProfileResponseDto, ) @@ -162,7 +170,8 @@ from .nodes import ( RestartNodeResponseDto, UpdateNodeRequestDto, UpdateNodeResponseDto, - RestartAllNodesRequestDto, + RestartAllNodesRequestDto, # Legacy alias, + RestartAllNodesRequestBodyDto, ) from .nodes_usage_history import ( GetNodeUserUsageByRangeResponseDto, @@ -184,11 +193,21 @@ from .subscription import ( ) from .subscriptions_settings import ( GetSubscriptionSettingsResponseDto, + ResponseModificationHeader, + ResponseModifications, + ResponseRule, + ResponseRuleCondition, + ResponseRules, SubscriptionSettingsResponseDto, UpdateSubscriptionSettingsRequestDto, UpdateSubscriptionSettingsResponseDto, ) from .subscriptions_template import ( + CreateSubscriptionTemplateRequestDto, + CreateSubscriptionTemplateResponseDto, + DeleteSubscriptionTemplateResponseDto, + GetTemplatesResponseDto, + TemplateInfoDto, GetTemplateResponseDto, TemplateResponseDto, UpdateTemplateRequestDto, @@ -212,6 +231,10 @@ from .system import ( GetNodesMetricsResponseDto, GetX25519KeyPairResponseDto, X25519KeyPair, + DebugSrrMatcherRequestDto, + DebugSrrMatcherResponseDto, + EncryptHappCryptoLinkRequestDto, + EncryptHappCryptoLinkResponseDto, ) from .users import ( ActiveInternalSquadDto, @@ -271,6 +294,56 @@ from .webhook import ( CrmEventDto, WebhookPayloadDto, ) +from .passkeys import ( + DeletePasskeyRequestDto, + DeletePasskeyResponseDto, + GetAllPasskeysResponseDto, + GetPasskeyRegistrationOptionsResponseDto, + PasskeyDto, + VerifyPasskeyRegistrationRequestDto, + VerifyPasskeyRegistrationResponseDto, +) +from .external_squads import ( + AddUsersToExternalSquadResponseDto, + CreateExternalSquadRequestDto, + CreateExternalSquadResponseDto, + DeleteExternalSquadResponseDto, + ExternalSquadDto, + ExternalSquadInfoDto, + ExternalSquadSubscriptionSettingsDto, + ExternalSquadTemplateDto, + GetExternalSquadByUuidResponseDto, + GetExternalSquadsResponseDto, + RemoveUsersFromExternalSquadResponseDto, + TemplateType, + UpdateExternalSquadRequestDto, + UpdateExternalSquadResponseDto, +) +from .snippets import ( + CreateSnippetRequestDto, + CreateSnippetResponseDto, + DeleteSnippetRequestDto, + DeleteSnippetResponseDto, + GetSnippetsResponseDto, + SnippetItem, + SnippetsData, + UpdateSnippetRequestDto, + UpdateSnippetResponseDto, +) +from .remnawave_settings import ( + BrandingSettings, + GetRemnawaveSettingsResponseDto, + GitHubOAuth2Settings, + OAuth2Settings, + PasskeySettings, + PasswordSettings, + PocketIdOAuth2Settings, + RemnawaveSettingsData, + TelegramAuthSettings, + UpdateRemnawaveSettingsRequestDto, + UpdateRemnawaveSettingsResponseDto, + YandexOAuth2Settings, +) __all__ = [ # Auth models @@ -283,6 +356,13 @@ __all__ = [ "TelegramCallbackRequestDto", "TelegramCallbackResponseDto", "LoginTelegramRequestDto", # Legacy alias + "OAuth2AuthorizeRequestDto", + "OAuth2AuthorizeResponseDto", + "OAuth2CallbackRequestDto", + "OAuth2CallbackResponseDto", + "VerifyPasskeyAuthenticationRequestDto", + "VerifyPasskeyAuthenticationResponseDto", + "GetPasskeyAuthenticationOptionsResponseDto", # Nodes models "CreateNodeRequestDto", "CreateNodeResponseDto", @@ -302,7 +382,8 @@ __all__ = [ "UpdateNodeResponseDto", "NodeConfigProfileDto", "NodeConfigProfileRequestDto", - "RestartAllNodesRequestDto", + "RestartAllNodesRequestDto", # Legacy alias + "RestartAllNodesRequestBodyDto", # Hosts models "CreateHostRequestDto", "CreateHostResponseDto", @@ -347,11 +428,21 @@ __all__ = [ "SubscriptionSettingsResponseDto", "UpdateSubscriptionSettingsRequestDto", "UpdateSubscriptionSettingsResponseDto", + "ResponseModificationHeader", + "ResponseModifications", + "ResponseRule", + "ResponseRuleCondition", + "ResponseRules", # Subscription template models "GetTemplateResponseDto", "TemplateResponseDto", "UpdateTemplateRequestDto", "UpdateTemplateResponseDto", + "CreateSubscriptionTemplateRequestDto", + "CreateSubscriptionTemplateResponseDto", + "DeleteSubscriptionTemplateResponseDto", + "GetTemplatesResponseDto", + "TemplateInfoDto", # System models "BandwidthStatistic", "BandwidthStatisticResponseDto", @@ -370,6 +461,10 @@ __all__ = [ "GetNodesMetricsResponseDto", "GetX25519KeyPairResponseDto", "X25519KeyPair", + "DebugSrrMatcherRequestDto", + "DebugSrrMatcherResponseDto", + "EncryptHappCryptoLinkRequestDto", + "EncryptHappCryptoLinkResponseDto", # XRay config models "ConfigResponseDto", # Legacy alias "GetConfigResponseDto", @@ -463,6 +558,7 @@ __all__ = [ "GetConfigProfileByUuidResponseDto", "GetInboundsByProfileUuidResponseDto", "InboundDto", + "NodesProfileDto", "UpdateConfigProfileRequestDto", "UpdateConfigProfileResponseDto", "GetAllConfigProfilesResponsePaginated", @@ -553,4 +649,56 @@ __all__ = [ # WEBHOOK PAYLOAD "WebhookPayloadDto", + + # Passkeys models + "DeletePasskeyRequestDto", + "DeletePasskeyResponseDto", + "GetAllPasskeysResponseDto", + "GetPasskeyRegistrationOptionsResponseDto", + "PasskeyDto", + "VerifyPasskeyRegistrationRequestDto", + "VerifyPasskeyRegistrationResponseDto", + + # External squads models + "AddUsersToExternalSquadResponseDto", + "CreateExternalSquadRequestDto", + "CreateExternalSquadResponseDto", + "DeleteExternalSquadResponseDto", + "ExternalSquadDto", + "ExternalSquadInfoDto", + "ExternalSquadSubscriptionSettingsDto", + "ExternalSquadTemplateDto", + "GetExternalSquadByUuidResponseDto", + "GetExternalSquadsResponseDto", + "RemoveUsersFromExternalSquadResponseDto", + "TemplateType", + "UpdateExternalSquadRequestDto", + "UpdateExternalSquadResponseDto", + + # Snippets models + + "CreateSnippetRequestDto", + "CreateSnippetResponseDto", + "DeleteSnippetRequestDto", + "DeleteSnippetResponseDto", + "GetSnippetsResponseDto", + "SnippetItem", + "SnippetsData", + "UpdateSnippetRequestDto", + "UpdateSnippetResponseDto", + + # Remnawave settings models + + "BrandingSettings", + "GetRemnawaveSettingsResponseDto", + "GitHubOAuth2Settings", + "OAuth2Settings", + "PasskeySettings", + "PasswordSettings", + "PocketIdOAuth2Settings", + "RemnawaveSettingsData", + "TelegramAuthSettings", + "UpdateRemnawaveSettingsRequestDto", + "UpdateRemnawaveSettingsResponseDto", + "YandexOAuth2Settings", ] diff --git a/remnawave/models/auth.py b/remnawave/models/auth.py index 2768054..48f21e6 100644 --- a/remnawave/models/auth.py +++ b/remnawave/models/auth.py @@ -1,7 +1,9 @@ -from typing import Annotated, Optional +from typing import Annotated, Any, Dict, Optional from pydantic import BaseModel, Field, StringConstraints +from remnawave.enums.auth import OAuth2Provider + class AuthTokenResponseData(BaseModel): access_token: str = Field(alias="accessToken") @@ -53,6 +55,47 @@ class TelegramCallbackResponseDto(AuthTokenResponseData): pass +# OAuth2 Authorization models +class OAuth2AuthorizeRequestDto(BaseModel): + """Request to initiate OAuth2 authorization""" + provider: OAuth2Provider + + +class OAuth2AuthorizeResponseDto(BaseModel): + """Response with OAuth2 authorization URL""" + authorization_url: Optional[str] = Field(alias="authorizationUrl") + + +# OAuth2 Callback models +class OAuth2CallbackRequestDto(BaseModel): + """Request for OAuth2 callback""" + provider: OAuth2Provider + code: str + state: str + + +class OAuth2CallbackResponseDto(BaseModel): + """Response with access token from OAuth2 callback""" + access_token: str = Field(alias="accessToken") + + +# Passkey Authentication models +class GetPasskeyAuthenticationOptionsResponseDto(BaseModel): + """Response with passkey authentication options""" + # Passkey options are complex WebAuthn objects, using Any for flexibility + response: Dict[str, Any] + + +class VerifyPasskeyAuthenticationRequestDto(BaseModel): + """Request to verify passkey authentication""" + # Passkey authentication response is complex WebAuthn object + response: Dict[str, Any] + + +class VerifyPasskeyAuthenticationResponseDto(BaseModel): + """Response with access token after successful passkey authentication""" + access_token: str = Field(alias="accessToken") + # Legacy alias for backward compatibility StatusResponseDto = GetStatusResponseDto -LoginTelegramRequestDto = TelegramCallbackRequestDto \ No newline at end of file +LoginTelegramRequestDto = TelegramCallbackRequestDto diff --git a/remnawave/models/bandwidthstats.py b/remnawave/models/bandwidthstats.py index 194ddbb..712e1eb 100644 --- a/remnawave/models/bandwidthstats.py +++ b/remnawave/models/bandwidthstats.py @@ -8,7 +8,7 @@ from pydantic import BaseModel, Field, RootModel class NodeUsageResponseDto(BaseModel): node_uuid: UUID = Field(alias="nodeUuid") node_name: str = Field(alias="nodeName") - total: float + total: int total_download: float = Field(alias="totalDownload") total_upload: float = Field(alias="totalUpload") human_readable_total: str = Field(alias="humanReadableTotal") @@ -65,7 +65,7 @@ class UserUsageByRangeItem(BaseModel): user_uuid: UUID = Field(alias="userUuid") node_uuid: UUID = Field(alias="nodeUuid") node_name: str = Field(alias="nodeName") - total: float + total: int date: str @@ -81,7 +81,7 @@ class NodeUserUsageItem(BaseModel): user_uuid: UUID = Field(alias="userUuid") username: str node_uuid: UUID = Field(alias="nodeUuid") - total: float + total: int date: str diff --git a/remnawave/models/config_profiles.py b/remnawave/models/config_profiles.py index 2753f48..46371fc 100644 --- a/remnawave/models/config_profiles.py +++ b/remnawave/models/config_profiles.py @@ -15,13 +15,17 @@ class InboundDto(BaseModel): port: Optional[int] = None raw_inbound: Optional[Any] = Field(None, alias="rawInbound") +class NodesProfileDto(BaseModel): + uuid: UUID + name: str + country_code: str = Field(alias="countryCode") class ConfigProfileDto(BaseModel): uuid: UUID name: str config: Dict[str, Any] inbounds: List[InboundDto] - nodes: List[Any] = [] # Can be empty list + nodes: List[NodesProfileDto] = [] created_at: datetime = Field(alias="createdAt") updated_at: datetime = Field(alias="updatedAt") diff --git a/remnawave/models/external_squads.py b/remnawave/models/external_squads.py new file mode 100644 index 0000000..665e237 --- /dev/null +++ b/remnawave/models/external_squads.py @@ -0,0 +1,102 @@ +from datetime import datetime +from enum import StrEnum +from typing import List, Optional +from uuid import UUID + +from pydantic import BaseModel, Field + + +class TemplateType(StrEnum): + """Template type enum""" + XRAY_JSON = "XRAY_JSON" + XRAY_BASE64 = "XRAY_BASE64" + MIHOMO = "MIHOMO" + STASH = "STASH" + CLASH = "CLASH" + SINGBOX = "SINGBOX" + + +class ExternalSquadInfoDto(BaseModel): + """External squad info""" + members_count: float = Field(alias="membersCount") + + +class ExternalSquadTemplateDto(BaseModel): + """External squad template""" + template_uuid: UUID = Field(alias="templateUuid") + template_type: TemplateType = Field(alias="templateType") + + +class ExternalSquadSubscriptionSettingsDto(BaseModel): + """External squad subscription settings""" + profile_title: str = Field(alias="profileTitle") + support_link: str = Field(alias="supportLink") + 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") + randomize_hosts: bool = Field(alias="randomizeHosts") + + +class ExternalSquadDto(BaseModel): + """External squad data model""" + uuid: UUID + name: str + info: ExternalSquadInfoDto + templates: List[ExternalSquadTemplateDto] + subscription_settings: Optional[ExternalSquadSubscriptionSettingsDto] = Field(alias="subscriptionSettings") + created_at: datetime = Field(alias="createdAt") + updated_at: datetime = Field(alias="updatedAt") + + +# Request/Response models +class GetExternalSquadsResponseDto(BaseModel): + """Response with all external squads""" + total: float + external_squads: List[ExternalSquadDto] = Field(alias="externalSquads") + + +class GetExternalSquadByUuidResponseDto(ExternalSquadDto): + """Response with external squad by UUID""" + pass + + +class CreateExternalSquadRequestDto(BaseModel): + """Request to create external squad""" + name: str = Field(min_length=2, max_length=30, pattern=r"^[A-Za-z0-9_\s-]+$") + + +class CreateExternalSquadResponseDto(ExternalSquadDto): + """Response after creating external squad""" + pass + + +class UpdateExternalSquadRequestDto(BaseModel): + """Request to update external squad""" + uuid: UUID + name: Optional[str] = Field(None, min_length=2, max_length=30, pattern=r"^[A-Za-z0-9_\s-]+$") + templates: Optional[List[ExternalSquadTemplateDto]] = None + subscription_settings: Optional[ExternalSquadSubscriptionSettingsDto] = Field(None, serialization_alias="subscriptionSettings") + + +class UpdateExternalSquadResponseDto(ExternalSquadDto): + """Response after updating external squad""" + pass + + +class DeleteExternalSquadResponseDto(BaseModel): + """Response after deleting external squad""" + is_deleted: bool = Field(alias="isDeleted") + + +class AddUsersToExternalSquadResponseDto(BaseModel): + """Response after adding users to external squad""" + event_sent: bool = Field(alias="eventSent") + + +class RemoveUsersFromExternalSquadResponseDto(BaseModel): + """Response after removing users from external squad""" + event_sent: bool = Field(alias="eventSent") \ No newline at end of file diff --git a/remnawave/models/hosts.py b/remnawave/models/hosts.py index 69a44db..6fb6ab7 100644 --- a/remnawave/models/hosts.py +++ b/remnawave/models/hosts.py @@ -23,9 +23,19 @@ class ReorderHostRequestDto(BaseModel): hosts: List[ReorderHostItem] +class HostInboundData(BaseModel): + config_profile_uuid: Optional[UUID] = Field(alias="configProfileUuid") + config_profile_inbound_uuid: Optional[UUID] = Field(alias="configProfileInboundUuid") + + +class CreateHostInboundData(BaseModel): + config_profile_uuid: UUID = Field(serialization_alias="configProfileUuid") + config_profile_inbound_uuid: UUID = Field(serialization_alias="configProfileInboundUuid") + + class UpdateHostRequestDto(BaseModel): uuid: UUID - inbound_uuid: Optional[UUID] = Field(None, serialization_alias="inboundUuid") + inbound: Optional[CreateHostInboundData] = None remark: Annotated[Optional[str], StringConstraints(max_length=40)] = None address: Optional[str] = None port: Optional[int] = None @@ -42,17 +52,7 @@ class UpdateHostRequestDto(BaseModel): server_description: Optional[str] = Field( None, serialization_alias="serverDescription", max_length=30 ) - mux_params: Optional[str] = Field( - None, - serialization_alias="muxParams", - ) - sockopt_params: Optional[str] = Field( - None, - serialization_alias="sockoptParams", - ) - tag: Optional[Annotated[str, StringConstraints(max_length=32)]] = Field( - None, serialization_alias="tag" - ) + tag: Optional[Annotated[str, StringConstraints(max_length=32, pattern=r"^[A-Z0-9_:]+$")]] = None is_hidden: Optional[bool] = Field( None, serialization_alias="isHidden", @@ -79,12 +79,20 @@ class UpdateHostRequestDto(BaseModel): None, serialization_alias="xHttpExtraParams", ) - nodes: Optional[List[str]] = None + mux_params: Optional[str] = Field( + None, + serialization_alias="muxParams", + ) + sockopt_params: Optional[str] = Field( + None, + serialization_alias="sockoptParams", + ) + nodes: Optional[List[UUID]] = None - -class HostInboundData(BaseModel): - config_profile_uuid: Optional[UUID] = Field(alias="configProfileUuid") - config_profile_inbound_uuid: Optional[UUID] = Field(alias="configProfileInboundUuid") + # Legacy compatibility properties + @property + def inbound_uuid(self) -> Optional[UUID]: + return self.inbound.config_profile_inbound_uuid if self.inbound else None class HostResponseDto(BaseModel): @@ -121,7 +129,7 @@ class HostResponseDto(BaseModel): ) shuffle_host: bool = Field(alias="shuffleHost") mihomo_x25519: bool = Field(alias="mihomoX25519") - nodes: List[str] + nodes: List[UUID] is_disabled: bool = Field( default=False, alias="isDisabled", @@ -149,54 +157,9 @@ class HostResponseDto(BaseModel): return self.inbound.config_profile_inbound_uuid -class HostsResponseDto(List[HostResponseDto]): - pass - - -class CreateHostResponseDto(HostResponseDto): - pass - - -class UpdateHostResponseDto(HostResponseDto): - pass - - -class GetAllHostTagsResponseDto(BaseModel): - tags: list[str] = None - - -class GetAllHostsResponseDto(RootModel[List[HostResponseDto]]): - root: List[HostResponseDto] - - def __iter__(self): - return iter(self.root) - - def __getitem__(self, item): - return self.root[item] - - -class GetOneHostResponseDto(HostResponseDto): - pass - - -class ReorderHostResponseDto(BaseModel): - is_updated: bool = Field(alias="isUpdated", default=True) - - -class DeleteHostResponseDto(BaseModel): - is_deleted: bool = Field(alias="isDeleted") - - -class CreateHostInboundData(BaseModel): - config_profile_uuid: Optional[UUID] = Field(serialization_alias="configProfileUuid") - config_profile_inbound_uuid: Optional[UUID] = Field( - serialization_alias="configProfileInboundUuid" - ) - - class CreateHostRequestDto(BaseModel): inbound: CreateHostInboundData - remark: Annotated[str, StringConstraints(max_length=40)] + remark: Annotated[str, StringConstraints(min_length=1, max_length=40)] address: str port: int path: Optional[str] = None @@ -219,9 +182,7 @@ class CreateHostRequestDto(BaseModel): server_description: Optional[str] = Field( None, serialization_alias="serverDescription", max_length=30 ) - tag: Optional[Annotated[str, StringConstraints(max_length=32)]] = Field( - None, serialization_alias="tag" - ) + tag: Optional[Annotated[str, StringConstraints(max_length=32, pattern=r"^[A-Z0-9_:]+$")]] = None vless_route_id: Optional[int] = Field( None, serialization_alias="vlessRouteId", @@ -236,7 +197,7 @@ class CreateHostRequestDto(BaseModel): False, serialization_alias="mihomoX25519", ) - nodes: List[str] = Field(default_factory=list) + nodes: List[UUID] = Field(default_factory=list) allow_insecure: bool = Field( False, serialization_alias="allowInsecure", @@ -278,4 +239,42 @@ class CreateHostRequestDto(BaseModel): or UUID("107541f1-ae1a-4e2d-9dec-7297557b5125"), config_profile_inbound_uuid=inbound_uuid, ) - super().__init__(**data) \ No newline at end of file + super().__init__(**data) + + +class GetAllHostTagsResponseDto(BaseModel): + tags: List[str] + + +class CreateHostResponseDto(HostResponseDto): + pass + + +class UpdateHostResponseDto(HostResponseDto): + pass + + +class GetAllHostsResponseDto(RootModel[List[HostResponseDto]]): + root: List[HostResponseDto] + + def __iter__(self): + return iter(self.root) + + def __getitem__(self, item): + return self.root[item] + + +class GetOneHostResponseDto(HostResponseDto): + pass + + +class ReorderHostResponseDto(BaseModel): + is_updated: bool = Field(alias="isUpdated", default=True) + + +class DeleteHostResponseDto(BaseModel): + is_deleted: bool = Field(alias="isDeleted") + + +# Legacy compatibility +HostsResponseDto = List[HostResponseDto] \ No newline at end of file diff --git a/remnawave/models/inbounds.py b/remnawave/models/inbounds.py index 53d3540..0b375cb 100644 --- a/remnawave/models/inbounds.py +++ b/remnawave/models/inbounds.py @@ -17,7 +17,7 @@ class InboundResponseDto(BaseModel): class AllInboundsData(BaseModel): - total: float + total: int inbounds: List[InboundResponseDto] @@ -26,7 +26,7 @@ class GetAllInboundsResponseDto(AllInboundsData): class InboundsByProfileData(BaseModel): - total: float + total: int inbounds: List[InboundResponseDto] diff --git a/remnawave/models/infra_billing.py b/remnawave/models/infra_billing.py index 82df79b..b1ddd0b 100644 --- a/remnawave/models/infra_billing.py +++ b/remnawave/models/infra_billing.py @@ -102,7 +102,7 @@ class UpdateInfraProviderResponseDto(InfraProviderDto): class AllInfraProvidersData(BaseModel): - total: float = Field(alias="total") + total: int = Field(alias="total") providers: List[InfraProviderDto] diff --git a/remnawave/models/nodes.py b/remnawave/models/nodes.py index 96e6209..a3773a1 100644 --- a/remnawave/models/nodes.py +++ b/remnawave/models/nodes.py @@ -28,8 +28,18 @@ class ReorderNodeItem(BaseModel): uuid: UUID +class NodeProviderDto(BaseModel): + """Node provider information""" + uuid: UUID + name: str + favicon_link: Optional[str] = Field(None, alias="faviconLink") + login_url: Optional[str] = Field(None, alias="loginUrl") + created_at: datetime = Field(alias="createdAt") + updated_at: datetime = Field(alias="updatedAt") + + class NodeConfigProfileDto(BaseModel): - active_config_profile_uuid: UUID = Field(alias="activeConfigProfileUuid") + active_config_profile_uuid: Optional[UUID] = Field(alias="activeConfigProfileUuid") active_inbounds: List[InboundsDto] = Field(alias="activeInbounds") @@ -39,30 +49,31 @@ class NodeConfigProfileRequestDto(BaseModel): class CreateNodeRequestDto(BaseModel): - name: Annotated[str, StringConstraints(min_length=3)] + name: Annotated[str, StringConstraints(min_length=3, max_length=30)] address: Annotated[str, StringConstraints(min_length=2)] - port: Optional[int] = Field(None, strict=True, ge=1) + port: Optional[int] = Field(None, ge=1, le=65535) is_traffic_tracking_active: Optional[bool] = Field( - None, + False, serialization_alias="isTrafficTrackingActive", ) traffic_limit_bytes: Optional[int] = Field( - None, serialization_alias="trafficLimitBytes", strict=True, ge=0 + None, serialization_alias="trafficLimitBytes", ge=0 ) notify_percent: Optional[int] = Field( - None, serialization_alias="notifyPercent", strict=True, ge=0 + None, serialization_alias="notifyPercent", ge=0, le=100 ) traffic_reset_day: Optional[int] = Field( - None, serialization_alias="trafficResetDay", strict=True, ge=1 + None, serialization_alias="trafficResetDay", ge=1, le=31 ) excluded_inbounds: Optional[List[UUID]] = Field( None, serialization_alias="excludedInbounds" ) country_code: Annotated[Optional[str], StringConstraints(max_length=2)] = Field( - None, serialization_alias="countryCode" + "XX", + serialization_alias="countryCode" ) consumption_multiplier: Optional[float] = Field( - None, serialization_alias="consumptionMultiplier" + None, serialization_alias="consumptionMultiplier", ge=0.1 ) config_profile: NodeConfigProfileRequestDto = Field( serialization_alias="configProfile" @@ -72,18 +83,20 @@ class CreateNodeRequestDto(BaseModel): class UpdateNodeRequestDto(BaseModel): uuid: UUID - name: Annotated[Optional[str], StringConstraints(min_length=3)] = None + name: Annotated[Optional[str], StringConstraints(min_length=3, max_length=30)] = None address: Annotated[Optional[str], StringConstraints(min_length=2)] = None - port: Optional[int] = None + port: Optional[float] = Field(None, ge=1, le=65535) # ИСПРАВЛЕН тип на float is_traffic_tracking_active: Optional[bool] = Field( None, serialization_alias="isTrafficTrackingActive" ) traffic_limit_bytes: Optional[float] = Field( - None, serialization_alias="trafficLimitBytes" + None, serialization_alias="trafficLimitBytes", ge=0 + ) + notify_percent: Optional[float] = Field( + None, serialization_alias="notifyPercent", ge=0, le=100 ) - notify_percent: Optional[float] = Field(None, serialization_alias="notifyPercent") traffic_reset_day: Optional[float] = Field( - None, serialization_alias="trafficResetDay" + None, serialization_alias="trafficResetDay", ge=1, le=31 ) excluded_inbounds: Optional[List[UUID]] = Field( None, serialization_alias="excludedInbounds" @@ -92,8 +105,12 @@ class UpdateNodeRequestDto(BaseModel): None, serialization_alias="countryCode" ) consumption_multiplier: Optional[float] = Field( - None, serialization_alias="consumptionMultiplier" + None, serialization_alias="consumptionMultiplier", ge=0.1 ) + config_profile: Optional[NodeConfigProfileRequestDto] = Field( + None, serialization_alias="configProfile" + ) + provider_uuid: Optional[UUID] = Field(None, serialization_alias="providerUuid") class ReorderNodeRequestDto(BaseModel): @@ -113,6 +130,7 @@ class NodeResponseDto(BaseModel): 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") + node_version: Optional[str] = Field(None, alias="nodeVersion") xray_uptime: str = Field(alias="xrayUptime") is_traffic_tracking_active: bool = Field(alias="isTrafficTrackingActive") traffic_reset_day: Optional[int] = Field(None, alias="trafficResetDay") @@ -129,16 +147,8 @@ class NodeResponseDto(BaseModel): created_at: datetime = Field(alias="createdAt") updated_at: datetime = Field(alias="updatedAt") config_profile: NodeConfigProfileDto = Field(alias="configProfile") - - -class NodesResponseDto(RootModel[List[NodeResponseDto]]): - root: List[NodeResponseDto] - - def __iter__(self): - return iter(self.root) - - def __getitem__(self, item): - return self.root[item] + provider_uuid: Optional[UUID] = Field(None, alias="providerUuid") + provider: Optional[NodeProviderDto] = None class CreateNodeResponseDto(NodeResponseDto): @@ -172,14 +182,16 @@ class DisableNodeResponseDto(NodeResponseDto): class RestartNodeResponseDto(BaseModel): - message: str + event_sent: bool = Field(alias="eventSent") class RestartAllNodesResponseDto(BaseModel): - message: str + event_sent: bool = Field(alias="eventSent") class ReorderNodeResponseDto(RootModel[List[NodeResponseDto]]): + root: List[NodeResponseDto] + def __iter__(self): return iter(self.root) @@ -194,5 +206,10 @@ class DeleteNodeResponseDto(BaseModel): return self.is_deleted -class RestartAllNodesRequestDto(BaseModel): +class RestartAllNodesRequestBodyDto(BaseModel): force_restart: bool = Field(default=False, alias="forceRestart") + + +# Для обратной совместимости +RestartAllNodesRequestDto = RestartAllNodesRequestBodyDto +NodesResponseDto = NodeResponseDto \ No newline at end of file diff --git a/remnawave/models/nodes_usage_history.py b/remnawave/models/nodes_usage_history.py index 9b6f53c..69111be 100644 --- a/remnawave/models/nodes_usage_history.py +++ b/remnawave/models/nodes_usage_history.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import List, Dict, Any from uuid import UUID @@ -18,11 +19,13 @@ class GetUserAccessibleNodesResponseDto(GetUserAccessibleNodesResponse): class NodeUsageDto(BaseModel): - date: str - upload: int - download: int - + """Individual node usage item""" + node_uuid: UUID = Field(alias="nodeUuid") + date: datetime + upload: int = Field(0, alias="totalBytes") + download: int = Field(0, alias="totalBytes") + class GetNodesUsageByRangeResponseDto(RootModel[List[NodeUsageDto]]): def __iter__(self): return iter(self.root) diff --git a/remnawave/models/passkeys.py b/remnawave/models/passkeys.py new file mode 100644 index 0000000..45d6586 --- /dev/null +++ b/remnawave/models/passkeys.py @@ -0,0 +1,46 @@ +from datetime import datetime +from typing import Any, Dict, List + +from pydantic import BaseModel, Field + + +class PasskeyDto(BaseModel): + """Passkey data model""" + id: str + name: str + created_at: datetime = Field(alias="createdAt") + last_used_at: datetime = Field(alias="lastUsedAt") + + +# Registration models +class GetPasskeyRegistrationOptionsResponseDto(BaseModel): + """Response with passkey registration options""" + # WebAuthn registration options are complex objects, using Any for flexibility + response: Dict[str, Any] + + +class VerifyPasskeyRegistrationRequestDto(BaseModel): + """Request to verify passkey registration""" + # WebAuthn registration response is complex object + response: Dict[str, Any] + + +class VerifyPasskeyRegistrationResponseDto(BaseModel): + """Response with passkey registration verification result""" + verified: bool + + +# Passkeys management models +class GetAllPasskeysResponseDto(BaseModel): + """Response with all user's passkeys""" + passkeys: List[PasskeyDto] + + +class DeletePasskeyRequestDto(BaseModel): + """Request to delete a passkey""" + id: str + + +class DeletePasskeyResponseDto(BaseModel): + """Response with updated passkeys list after deletion""" + passkeys: List[PasskeyDto] \ No newline at end of file diff --git a/remnawave/models/remnawave_settings.py b/remnawave/models/remnawave_settings.py new file mode 100644 index 0000000..c73cbf3 --- /dev/null +++ b/remnawave/models/remnawave_settings.py @@ -0,0 +1,88 @@ +from typing import List, Optional + +from pydantic import BaseModel, Field, HttpUrl + + +class PasskeySettings(BaseModel): + """Passkey authentication settings""" + enabled: bool + rp_id: Optional[str] = Field(None, alias="rpId") + origin: Optional[str] = None + + +class GitHubOAuth2Settings(BaseModel): + """GitHub OAuth2 settings""" + enabled: bool + client_id: Optional[str] = Field(None, alias="clientId") + client_secret: Optional[str] = Field(None, alias="clientSecret") + allowed_emails: List[str] = Field(alias="allowedEmails") + + +class PocketIdOAuth2Settings(BaseModel): + """PocketID OAuth2 settings""" + enabled: bool + client_id: Optional[str] = Field(None, alias="clientId") + client_secret: Optional[str] = Field(None, alias="clientSecret") + plain_domain: Optional[str] = Field(None, alias="plainDomain") + allowed_emails: List[str] = Field(alias="allowedEmails") + + +class YandexOAuth2Settings(BaseModel): + """Yandex OAuth2 settings""" + enabled: bool + client_id: Optional[str] = Field(None, alias="clientId") + client_secret: Optional[str] = Field(None, alias="clientSecret") + allowed_emails: List[str] = Field(alias="allowedEmails") + + +class OAuth2Settings(BaseModel): + """OAuth2 authentication settings""" + github: GitHubOAuth2Settings + pocketid: PocketIdOAuth2Settings + yandex: YandexOAuth2Settings + + +class TelegramAuthSettings(BaseModel): + """Telegram authentication settings""" + enabled: bool + bot_token: Optional[str] = Field(None, alias="botToken") + admin_ids: List[str] = Field(alias="adminIds") + + +class PasswordSettings(BaseModel): + """Password authentication settings""" + enabled: bool + + +class BrandingSettings(BaseModel): + """Branding settings""" + title: Optional[str] = None + logo_url: Optional[HttpUrl] = Field(None, alias="logoUrl") + + +class RemnawaveSettingsData(BaseModel): + """Remnawave settings data""" + passkey_settings: Optional[PasskeySettings] = Field(None, alias="passkeySettings") + oauth2_settings: Optional[OAuth2Settings] = Field(None, alias="oauth2Settings") + tg_auth_settings: Optional[TelegramAuthSettings] = Field(None, alias="tgAuthSettings") + password_settings: Optional[PasswordSettings] = Field(None, alias="passwordSettings") + branding_settings: Optional[BrandingSettings] = Field(None, alias="brandingSettings") + + +class GetRemnawaveSettingsResponseDto(BaseModel): + """Get Remnawave settings response""" + response: RemnawaveSettingsData + + +class UpdateRemnawaveSettingsRequestDto(BaseModel): + """Update Remnawave settings request""" + passkey_settings: Optional[PasskeySettings] = Field(None, serialization_alias="passkeySettings") + oauth2_settings: Optional[OAuth2Settings] = Field(None, serialization_alias="oauth2Settings") + tg_auth_settings: Optional[TelegramAuthSettings] = Field(None, serialization_alias="tgAuthSettings") + password_settings: Optional[PasswordSettings] = Field(None, serialization_alias="passwordSettings") + branding_settings: Optional[BrandingSettings] = Field(None, serialization_alias="brandingSettings") + + +class UpdateRemnawaveSettingsResponseDto(BaseModel): + """Update Remnawave settings response""" + response: RemnawaveSettingsData \ No newline at end of file diff --git a/remnawave/models/snippets.py b/remnawave/models/snippets.py new file mode 100644 index 0000000..40fc869 --- /dev/null +++ b/remnawave/models/snippets.py @@ -0,0 +1,56 @@ +from typing import Annotated, Any, List + +from pydantic import BaseModel, Field, StringConstraints, RootModel + + +class SnippetItem(BaseModel): + """Individual snippet item""" + name: str + snippet: Any # Can be any JSON object or array + + +class SnippetsData(BaseModel): + """Snippets response data""" + total: int + snippets: List[SnippetItem] + + +# Изменяем структуру - API возвращает данные напрямую +class GetSnippetsResponseDto(SnippetsData): + """Get all snippets response - extends SnippetsData directly""" + pass + + +class CreateSnippetResponseDto(SnippetsData): + """Create snippet response - extends SnippetsData directly""" + pass + + +class UpdateSnippetResponseDto(SnippetsData): + """Update snippet response - extends SnippetsData directly""" + pass + + +class DeleteSnippetResponseDto(SnippetsData): + """Delete snippet response - extends SnippetsData directly""" + pass + + +class CreateSnippetRequestDto(BaseModel): + """Create snippet request""" + name: Annotated[str, StringConstraints(min_length=2, max_length=255, pattern=r"^[A-Za-z0-9_\s-]+$")] + snippet: List[dict] # Array of objects + + +class UpdateSnippetRequestDto(BaseModel): + """Update snippet request""" + name: Annotated[str, StringConstraints(min_length=2, max_length=255, pattern=r"^[A-Za-z0-9_\s-]+$")] + snippet: List[dict] # Array of objects + + +class DeleteSnippetRequestDto(BaseModel): + """Delete snippet request""" + name: Annotated[str, StringConstraints(min_length=2, max_length=255, pattern=r"^[A-Za-z0-9_\s-]+$")] + +class DeleteSnippetResponseDto(SnippetsData): + """Delete snippet response""" \ No newline at end of file diff --git a/remnawave/models/subscription.py b/remnawave/models/subscription.py index 00f1fcd..f337634 100644 --- a/remnawave/models/subscription.py +++ b/remnawave/models/subscription.py @@ -185,7 +185,7 @@ class SubscriptionWithoutHapp(BaseModel): class GetAllSubscriptionsResponseDto(BaseModel): subscriptions: List[SubscriptionWithoutHapp] - total: float + total: int class GetSubscriptionByUsernameResponseDto(BaseModel): diff --git a/remnawave/models/subscription_request_history.py b/remnawave/models/subscription_request_history.py index f9ee175..3987304 100644 --- a/remnawave/models/subscription_request_history.py +++ b/remnawave/models/subscription_request_history.py @@ -15,7 +15,7 @@ class SubscriptionRequestHistoryRecord(BaseModel): class SubscriptionRequestHistoryData(BaseModel): records: List[SubscriptionRequestHistoryRecord] - total: float + total: int class GetAllSubscriptionRequestHistoryResponseDto(SubscriptionRequestHistoryData): diff --git a/remnawave/models/subscriptions_settings.py b/remnawave/models/subscriptions_settings.py index 4b43b4b..52ff381 100644 --- a/remnawave/models/subscriptions_settings.py +++ b/remnawave/models/subscriptions_settings.py @@ -4,6 +4,56 @@ from uuid import UUID from pydantic import BaseModel, Field, StringConstraints +from remnawave.enums import ( + ResponseRuleConditionOperator, + ResponseRuleOperator, + ResponseRuleVersion, + ResponseType, +) + + +class ResponseRuleCondition(BaseModel): + """Condition to check against request headers""" + header_name: Annotated[str, StringConstraints(pattern=r"^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$")] = Field( + alias="headerName" + ) + operator: ResponseRuleConditionOperator + value: Annotated[str, StringConstraints(min_length=1, max_length=255)] + case_sensitive: bool = Field(alias="caseSensitive") + + +class ResponseModificationHeader(BaseModel): + """Response header modification""" + key: Annotated[str, StringConstraints(pattern=r"^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$")] + value: Annotated[str, StringConstraints(min_length=1)] + + +class ResponseModifications(BaseModel): + """Response modifications to apply when rule matches""" + headers: Optional[List[ResponseModificationHeader]] = None + subscription_template: Optional[Annotated[str, StringConstraints(min_length=1)]] = Field( + None, alias="subscriptionTemplate" + ) + + +class ResponseRule(BaseModel): + """Individual response rule configuration""" + name: Annotated[str, StringConstraints(min_length=1, max_length=50)] + description: Optional[Annotated[str, StringConstraints(min_length=1, max_length=250)]] = None + enabled: bool + operator: ResponseRuleOperator + conditions: List[ResponseRuleCondition] + response_type: ResponseType = Field(alias="responseType") + response_modifications: Optional[ResponseModifications] = Field( + None, alias="responseModifications" + ) + + +class ResponseRules(BaseModel): + """Response rules configuration""" + version: ResponseRuleVersion + rules: List[ResponseRule] + class SubscriptionSettingsResponseDto(BaseModel): uuid: UUID @@ -27,6 +77,7 @@ class SubscriptionSettingsResponseDto(BaseModel): None, alias="customResponseHeaders" ) randomize_hosts: bool = Field(alias="randomizeHosts") + response_rules: Optional[ResponseRules] = Field(None, alias="responseRules") created_at: datetime = Field(alias="createdAt") updated_at: datetime = Field(alias="updatedAt") @@ -41,37 +92,38 @@ class UpdateSubscriptionSettingsResponseDto(SubscriptionSettingsResponseDto): class UpdateSubscriptionSettingsRequestDto(BaseModel): uuid: UUID - profile_title: Optional[str] = Field(None, serialization_alias="profileTitle") - support_link: Optional[str] = Field(None, serialization_alias="supportLink") + profile_title: Optional[str] = Field(None, alias="profileTitle") + support_link: Optional[str] = Field(None, alias="supportLink") profile_update_interval: Optional[int] = Field( - None, serialization_alias="profileUpdateInterval" + None, alias="profileUpdateInterval" ) is_profile_webpage_url_enabled: Optional[bool] = Field( - None, serialization_alias="isProfileWebpageUrlEnabled" + None, alias="isProfileWebpageUrlEnabled" ) serve_json_at_base_subscription: Optional[bool] = Field( - None, serialization_alias="serveJsonAtBaseSubscription" + None, alias="serveJsonAtBaseSubscription" ) add_username_to_base_subscription: Optional[bool] = Field( - None, serialization_alias="addUsernameToBaseSubscription" + None, alias="addUsernameToBaseSubscription" ) is_show_custom_remarks: Optional[bool] = Field( - None, serialization_alias="isShowCustomRemarks" + None, alias="isShowCustomRemarks" ) happ_announce: Annotated[Optional[str], StringConstraints(max_length=200)] = Field( - None, serialization_alias="happAnnounce" + None, alias="happAnnounce" ) - happ_routing: Optional[str] = Field(None, serialization_alias="happRouting") + happ_routing: Optional[str] = Field(None, alias="happRouting") expired_users_remarks: Optional[List[str]] = Field( - None, serialization_alias="expiredUsersRemarks" + None, alias="expiredUsersRemarks" ) limited_users_remarks: Optional[List[str]] = Field( - None, serialization_alias="limitedUsersRemarks" + None, alias="limitedUsersRemarks" ) disabled_users_remarks: Optional[List[str]] = Field( - None, serialization_alias="disabledUsersRemarks" + None, alias="disabledUsersRemarks" ) custom_response_headers: Optional[Dict[str, str]] = Field( - None, serialization_alias="customResponseHeaders" + None, alias="customResponseHeaders" ) - randomize_hosts: Optional[bool] = Field(None, serialization_alias="randomizeHosts") \ No newline at end of file + randomize_hosts: Optional[bool] = Field(None, alias="randomizeHosts") + response_rules: Optional[ResponseRules] = Field(None, alias="responseRules") \ No newline at end of file diff --git a/remnawave/models/subscriptions_template.py b/remnawave/models/subscriptions_template.py index 23f082c..1ff1102 100644 --- a/remnawave/models/subscriptions_template.py +++ b/remnawave/models/subscriptions_template.py @@ -1,13 +1,23 @@ -from typing import Any, Optional +from typing import Annotated, Any, List, Optional from uuid import UUID -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, StringConstraints from remnawave.enums import TemplateType class TemplateResponseDto(BaseModel): uuid: UUID + name: str + template_type: TemplateType = Field(alias="templateType") + template_json: Optional[Any] = Field(None, alias="templateJson") + encoded_template_yaml: Optional[str] = Field(None, alias="encodedTemplateYaml") + + +class TemplateInfoDto(BaseModel): + """Template info without content - used in list responses""" + uuid: UUID + name: str template_type: TemplateType = Field(alias="templateType") template_json: Optional[Any] = Field(None, alias="templateJson") encoded_template_yaml: Optional[str] = Field(None, alias="encodedTemplateYaml") @@ -16,9 +26,26 @@ class TemplateResponseDto(BaseModel): class GetTemplateResponseDto(TemplateResponseDto): pass +class GetTemplatesData(BaseModel): + total: int + templates: List[TemplateInfoDto] + +class GetTemplatesResponseDto(GetTemplatesData): + pass + + +class CreateSubscriptionTemplateRequestDto(BaseModel): + name: Annotated[str, StringConstraints(min_length=2, max_length=255, pattern=r"^[A-Za-z0-9_\s-]+$")] + template_type: TemplateType = Field(serialization_alias="templateType") + + +class CreateSubscriptionTemplateResponseDto(TemplateResponseDto): + pass + class UpdateTemplateRequestDto(BaseModel): - template_type: TemplateType = Field(serialization_alias="templateType") + uuid: UUID + name: Optional[Annotated[str, StringConstraints(min_length=2, max_length=255, pattern=r"^[A-Za-z0-9_\s-]+$")]] = None template_json: Optional[dict] = Field(None, serialization_alias="templateJson") encoded_template_yaml: Optional[str] = Field( None, serialization_alias="encodedTemplateYaml" @@ -27,3 +54,23 @@ class UpdateTemplateRequestDto(BaseModel): class UpdateTemplateResponseDto(TemplateResponseDto): pass + +class DeleteTemplateData(BaseModel): + is_deleted: bool = Field(alias="isDeleted") + + +class DeleteSubscriptionTemplateResponseDto(DeleteTemplateData): + pass + + +# Legacy aliases for backward compatibility +class UpdateTemplateRequestDtoLegacy(BaseModel): + template_type: TemplateType = Field(serialization_alias="templateType") + template_json: Optional[dict] = Field(None, serialization_alias="templateJson") + encoded_template_yaml: Optional[str] = Field( + None, serialization_alias="encodedTemplateYaml" + ) + + +class UpdateTemplateResponseDtoLegacy(TemplateResponseDto): + pass \ No newline at end of file diff --git a/remnawave/models/system.py b/remnawave/models/system.py index f84615b..b8c7159 100644 --- a/remnawave/models/system.py +++ b/remnawave/models/system.py @@ -1,8 +1,11 @@ import datetime -from typing import List +from typing import Dict, List, Optional from pydantic import BaseModel, Field +from remnawave.enums import ResponseType +from remnawave.models.subscriptions_settings import ResponseRule, ResponseRules + class NodeStatistic(BaseModel): node_name: str = Field(alias="nodeName") @@ -105,26 +108,60 @@ class GetRemnawaveHealthResponseDto(BaseModel): pm2_stats: List[PM2Stat] = Field(alias="pm2Stats") + class NodeMetric(BaseModel): - uuid: str - name: str - address: str - is_online: bool = Field(alias="isOnline") - cpu_usage: float = Field(alias="cpuUsage") - memory_usage: float = Field(alias="memoryUsage") - network_upload: int = Field(alias="networkUpload") - network_download: int = Field(alias="networkDownload") - uptime: int - last_seen: datetime.datetime = Field(alias="lastSeen") - connected_users: int = Field(alias="connectedUsers") + """Node metric data""" + uuid: str = Field(alias="nodeUuid") + name: Optional[str] = None + address: Optional[str] = None + is_online: Optional[bool] = Field(None, alias="isOnline") + cpu_usage: Optional[float] = Field(None, alias="cpuUsage") + memory_usage: Optional[float] = Field(None, alias="memoryUsage") + network_upload: Optional[int] = Field(None, alias="networkUpload") + network_download: Optional[int] = Field(None, alias="networkDownload") + uptime: Optional[int] = None + last_seen: Optional[datetime.datetime] = Field(None, alias="lastSeen") + connected_users: Optional[int] = Field(None, alias="connectedUsers") + upload: Optional[str] = None + download: Optional[str] = None class GetNodesMetricsResponseDto(BaseModel): nodes: List[NodeMetric] + class X25519KeyPair(BaseModel): public_key: str = Field(alias="publicKey") private_key: str = Field(alias="privateKey") + class GetX25519KeyPairResponseDto(BaseModel): - key_pairs: List[X25519KeyPair] = Field(alias="keyPairs") \ No newline at end of file + key_pairs: List[X25519KeyPair] = Field(alias="keyPairs") + + +class EncryptHappCryptoLinkRequestDto(BaseModel): + link_to_encrypt: str = Field(serialization_alias="linkToEncrypt") + + +class EncryptHappCryptoLinkData(BaseModel): + encrypted_link: str = Field(alias="encryptedLink") + + +class EncryptHappCryptoLinkResponseDto(BaseModel): + response: EncryptHappCryptoLinkData + + +class DebugSrrMatcherRequestDto(BaseModel): + response_rules: ResponseRules = Field(serialization_alias="responseRules") + + +class DebugSrrMatcherData(BaseModel): + matched: bool + response_type: ResponseType = Field(alias="responseType") + matched_rule: Optional[ResponseRule] = Field(alias="matchedRule") + input_headers: Dict[str, str] = Field(alias="inputHeaders") + output_headers: Dict[str, str] = Field(alias="outputHeaders") + + +class DebugSrrMatcherResponseDto(BaseModel): + response: DebugSrrMatcherData \ No newline at end of file diff --git a/remnawave/models/users.py b/remnawave/models/users.py index 4157728..ef7bf9f 100644 --- a/remnawave/models/users.py +++ b/remnawave/models/users.py @@ -68,6 +68,7 @@ class CreateUserRequestDto(BaseModel): active_internal_squads: list[str] | None = Field( None, serialization_alias="activeInternalSquads" ) + external_squad_uuid: UUID | None = Field(None, alias="externalSquadUuid") uuid: Optional[UUID] = Field(None, description="UUID of the user. Optional. If not provided, a new UUID will be generated by Remnawave.") @@ -128,6 +129,7 @@ class UserResponseDto(BaseModel): ) happ: HappCrypto | None = Field(None, alias="happ") tag: str | None = Field(None, alias="tag") + external_squad_uuid: UUID | None = Field(None, alias="externalSquadUuid") created_at: datetime = Field(alias="createdAt") updated_at: datetime = Field(alias="updatedAt") @@ -158,7 +160,7 @@ class TelegramUserResponseDto(RootModel[list[UserResponseDto]]): class UsersResponseDto(BaseModel): users: list[UserResponseDto] - total: float + total: int class DeleteUserResponseDto(BaseModel): @@ -229,7 +231,7 @@ class SubscriptionRequestRecord(BaseModel): class SubscriptionRequestsResponseData(BaseModel): - total: float + total: int records: List[SubscriptionRequestRecord] diff --git a/remnawave/models/users_bulk_actions.py b/remnawave/models/users_bulk_actions.py index b76fe93..30aad18 100644 --- a/remnawave/models/users_bulk_actions.py +++ b/remnawave/models/users_bulk_actions.py @@ -33,6 +33,13 @@ class UpdateUserFields(BaseModel): ) 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 + ) + telegram_id: Optional[int] = Field( + None, serialization_alias="telegramId" + ) + class BulkAllUpdateUsersRequestDto(BaseModel): @@ -66,6 +73,10 @@ class BulkAllUpdateUsersRequestDto(BaseModel): serialization_alias="telegramId" ) + hwid_device_limit: Optional[int] = Field( + None, serialization_alias="hwidDeviceLimit", strict=True, ge=0 + ) + email: Optional[str] = None tag: Optional[TagStr] = Field( diff --git a/tests/test_snippets.py b/tests/test_snippets.py new file mode 100644 index 0000000..743ae71 --- /dev/null +++ b/tests/test_snippets.py @@ -0,0 +1,77 @@ +import pytest + +from remnawave.models import ( + CreateSnippetRequestDto, + DeleteSnippetRequestDto, + GetSnippetsResponseDto, + UpdateSnippetRequestDto, +) + +def random_string(length=10): + import random + import string + return ''.join(random.choices(string.ascii_letters + string.digits, k=length)) + +@pytest.mark.asyncio +async def test_snippets_full_workflow(remnawave): + # Test getting all snippets + snippets = await remnawave.snippets.get_snippets() + assert isinstance(snippets, GetSnippetsResponseDto) + initial_count = snippets.total + + # Test creating a snippet with unique name + rand_name = random_string() + create_request = CreateSnippetRequestDto( + name=rand_name, + snippet=[ + {"type": "vmess", "port": 443}, + {"type": "vless", "encryption": "none"} + ] + ) + created = await remnawave.snippets.create_snippet(create_request) + assert created.total == initial_count + 1 + + # Verify snippet was created + snippets_after_create = await remnawave.snippets.get_snippets() + snippet_names = [s.name for s in snippets_after_create.snippets] + assert rand_name in snippet_names + + # Test updating the snippet + update_request = UpdateSnippetRequestDto( + name=rand_name, + snippet=[ + {"type": "shadowsocks", "method": "aes-256-gcm"}, + {"type": "trojan", "password": "test-password"} + ] + ) + updated = await remnawave.snippets.update_snippet(update_request) + assert updated.total == initial_count + 1 + + # Test deleting the snippet + delete_request = DeleteSnippetRequestDto(name=rand_name) + deleted = await remnawave.snippets.delete_snippet_by_name(delete_request) + assert deleted.total == initial_count + + # Verify snippet was deleted + snippets_after_delete = await remnawave.snippets.get_snippets() + snippet_names_after = [s.name for s in snippets_after_delete.snippets] + assert rand_name not in snippet_names_after + + +@pytest.mark.asyncio +async def test_snippet_name_validation(remnawave): + """Test that snippet names are properly validated""" + # Valid names + valid_names = ["Test Snippet", "My_Snippet", "Snippet-123", "A B C"] + + for name in valid_names: + request = CreateSnippetRequestDto(name=name, snippet=[{"test": "data"}]) + # Should not raise validation error + assert request.name == name + + # Invalid names would be caught by Pydantic validation + with pytest.raises(ValueError): + CreateSnippetRequestDto(name="x", snippet=[]) # Too short + + with pytest.raises(ValueError): + CreateSnippetRequestDto(name="", snippet=[]) # Empty name \ No newline at end of file diff --git a/tests/test_subscriptions_template.py b/tests/test_subscriptions_template.py index 54b7ff3..3093bbc 100644 --- a/tests/test_subscriptions_template.py +++ b/tests/test_subscriptions_template.py @@ -1,19 +1,91 @@ import pytest - from remnawave.enums import TemplateType -from remnawave.models import GetTemplateResponseDto, UpdateTemplateRequestDto, UpdateTemplateResponseDto +from remnawave.models import ( + CreateSubscriptionTemplateRequestDto, + CreateSubscriptionTemplateResponseDto, + DeleteSubscriptionTemplateResponseDto, + GetTemplateResponseDto, + GetTemplatesResponseDto, + UpdateTemplateRequestDto, + UpdateTemplateResponseDto, +) + +def random_string(length=10): + import random + import string + return ''.join(random.choices(string.ascii_letters + string.digits, k=length)) + +@pytest.mark.asyncio +async def test_get_all_templates(remnawave): + """Проверка получения всех шаблонов""" + templates = await remnawave.subscriptions_template.get_all_templates() + assert isinstance(templates, GetTemplatesResponseDto) @pytest.mark.asyncio -async def test_subscriptions_template(remnawave): - template_type: TemplateType = TemplateType.SINGBOX - template = await remnawave.subscriptions_template.get_template( - template_type=template_type +async def test_create_template(remnawave): + """Проверка создания шаблона""" + rand_name = random_string() + create_request = CreateSubscriptionTemplateRequestDto( + name=rand_name, + template_type=TemplateType.SINGBOX, + ) + created_template = await remnawave.subscriptions_template.create_template(create_request) + assert isinstance(created_template, CreateSubscriptionTemplateResponseDto) + assert created_template.name == rand_name + assert created_template.template_type == TemplateType.SINGBOX + + # Удаляем после проверки + await remnawave.subscriptions_template.delete_template(str(created_template.uuid)) + + +@pytest.fixture +async def created_template(remnawave): + """Фикстура: создать временный шаблон и удалить после теста""" + create_request = CreateSubscriptionTemplateRequestDto( + name="Temp Template", + template_type=TemplateType.SINGBOX, + ) + template = await remnawave.subscriptions_template.create_template(create_request) + yield template + await remnawave.subscriptions_template.delete_template(str(template.uuid)) + + +@pytest.mark.asyncio +async def test_get_template_by_uuid(remnawave, created_template): + """Проверка получения шаблона по UUID""" + template = await remnawave.subscriptions_template.get_template_by_uuid( + str(created_template.uuid) ) assert isinstance(template, GetTemplateResponseDto) + assert template.uuid == created_template.uuid - update_template = await remnawave.subscriptions_template.update_template( - UpdateTemplateRequestDto(template_type=template_type) + +@pytest.mark.asyncio +async def test_update_template(remnawave, created_template): + """Проверка обновления шаблона""" + update_request = UpdateTemplateRequestDto( + uuid=created_template.uuid, + name="Updated Template Name", ) - assert isinstance(update_template, UpdateTemplateResponseDto) - assert update_template.template_type == template_type + updated_template = await remnawave.subscriptions_template.update_template(update_request) + assert isinstance(updated_template, UpdateTemplateResponseDto) + assert updated_template.name == "Updated Template Name" + + +@pytest.mark.asyncio +async def test_delete_template(remnawave): + """Проверка удаления шаблона""" + # Сначала создаем + create_request = CreateSubscriptionTemplateRequestDto( + name="Temp Delete Template", + template_type=TemplateType.SINGBOX, + ) + created = await remnawave.subscriptions_template.create_template(create_request) + + # Теперь удаляем + delete_response = await remnawave.subscriptions_template.delete_template( + str(created.uuid) + ) + assert isinstance(delete_response, DeleteSubscriptionTemplateResponseDto) + assert delete_response.is_deleted is True \ No newline at end of file