diff --git a/README.md b/README.md index 31ba31b..525ab4a 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ pip install git+https://github.com/remnawave/python-sdk.git@development | Contract Version | Remnawave Panel Version | | ---------------- | ----------------------- | +| 2.8.0 | >=2.8.0 | | 2.7.0 | >=2.7.0 | | 2.6.3 | >=2.6.3 | | 2.6.2 | >=2.6.2 | diff --git a/poetry.lock b/poetry.lock index df7809a..6329b37 100644 --- a/poetry.lock +++ b/poetry.lock @@ -894,5 +894,5 @@ typing-extensions = ">=4.12.0" [metadata] lock-version = "2.1" -python-versions = ">=3.11,<4.0" -content-hash = "2e64664ad5ef0cd863e8a3898493f4cdcf0544ada35d9ecad6aa686b32bdb1ac" +python-versions = ">=3.11,<3.15" +content-hash = "7b2d287bb0036b4254e0589beb3d86856735c088d39dcbf1bb2c965ce04a045f" diff --git a/pyproject.toml b/pyproject.toml index 57b4af1..2a54773 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,18 @@ [project] name = "remnawave" -version = "2.7.1" -description = "A Python SDK for interacting with the Remnawave API v2.7.1." +version = "2.8.0" +description = "A Python SDK for interacting with the Remnawave API v2.8.0." authors = [ {name = "Artem",email = "dev@forestsnet.com"} ] license = { text = "MIT" } readme = "README.md" -requires-python = ">=3.11,<3.14" +requires-python = ">=3.11,<3.15" dependencies = [ "rapid-api-client (==0.6.0)", "orjson (>=3.10.15,<4.0.0)", "httpx (>=0.27.2,<0.28.0)", "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"] @@ -26,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Framework :: AsyncIO", diff --git a/remnawave/controllers/api_tokens_management.py b/remnawave/controllers/api_tokens_management.py index 0dcf62e..b4d3f37 100644 --- a/remnawave/controllers/api_tokens_management.py +++ b/remnawave/controllers/api_tokens_management.py @@ -9,6 +9,7 @@ from remnawave.models import ( CreateApiTokenResponseDto, DeleteApiTokenResponseDto, FindAllApiTokensResponseDto, + GetApiTokenScopesResponseDto, ) from remnawave.rapid import BaseController, delete, get, post @@ -36,3 +37,10 @@ class APITokensManagementController(BaseController): ) -> FindAllApiTokensResponseDto: """Get all API tokens""" ... + + @get("/tokens/scopes", response_class=GetApiTokenScopesResponseDto) + async def get_scopes( + self, + ) -> GetApiTokenScopesResponseDto: + """Get available API token scopes""" + ... diff --git a/remnawave/controllers/bandwidthstats.py b/remnawave/controllers/bandwidthstats.py index 392aa15..8445d46 100644 --- a/remnawave/controllers/bandwidthstats.py +++ b/remnawave/controllers/bandwidthstats.py @@ -1,6 +1,7 @@ from typing import Annotated from rapid_api_client import Path, Query +from rapid_api_client.annotations import PydanticBody from remnawave.models.bandwidthstats import ( GetLegacyStatsNodesUsersUsageResponseDto, @@ -11,10 +12,12 @@ from remnawave.models.bandwidthstats import ( GetStatsNodesRealtimeUsageResponseDto, GetStatsNodesUsageResponseDto, GetStatsNodeUsersUsageResponseDto, + GetStatsNodesUsersUsageRequestDto, + GetStatsNodesUsersUsageResponseDto, GetStatsUserUsageResponseDto, GetUserUsageByRangeResponseDto, ) -from remnawave.rapid import BaseController, get +from remnawave.rapid import BaseController, get, post class BandWidthStatsController(BaseController): @@ -70,6 +73,17 @@ class BandWidthStatsController(BaseController): """Get Node Users Usage by Node UUID""" ... + @post("/bandwidth-stats/nodes/users", response_class=GetStatsNodesUsersUsageResponseDto) + async def get_stats_nodes_users_usage( + self, + body: Annotated[GetStatsNodesUsersUsageRequestDto, PydanticBody()], + top_users_limit: Annotated[int, Query(description="Limit of top users to return", alias="topUsersLimit")], + start: Annotated[str, Query(description="Start date (YYYY-MM-DD)")], + end: Annotated[str, Query(description="End date (YYYY-MM-DD)")], + ) -> GetStatsNodesUsersUsageResponseDto: + """Get Nodes Users Usage by Nodes UUIDs""" + ... + @get("/bandwidth-stats/users/{uuid}", response_class=GetStatsUserUsageResponseDto) async def get_stats_user_usage( self, diff --git a/remnawave/controllers/hosts_bulk_actions.py b/remnawave/controllers/hosts_bulk_actions.py index 6f4d759..236d5ac 100644 --- a/remnawave/controllers/hosts_bulk_actions.py +++ b/remnawave/controllers/hosts_bulk_actions.py @@ -7,12 +7,10 @@ from remnawave.models import ( BulkDeleteHostsResponseDto, BulkDisableHostsResponseDto, BulkEnableHostsResponseDto, - SetInboundToManyHostsRequestDto, - SetInboundToManyHostsResponseDto, - SetPortToManyHostsResponseDto, - SetPortToManyHostsRequestDto + UpdateManyHostsRequestDto, + UpdateManyHostsResponseDto, ) -from remnawave.rapid import AttributeBody, BaseController, post +from remnawave.rapid import AttributeBody, BaseController, patch, post class HostsBulkActionsController(BaseController): @@ -40,21 +38,10 @@ class HostsBulkActionsController(BaseController): """Enable many hosts""" ... - @post( - "/hosts/bulk/set-inbound", - response_class=SetInboundToManyHostsResponseDto, - ) - async def set_inbound_to_hosts( + @patch("/hosts/bulk/update", response_class=UpdateManyHostsResponseDto) + async def update_hosts( self, - body: Annotated[SetInboundToManyHostsRequestDto, PydanticBody()], - ) -> SetInboundToManyHostsResponseDto: - """Set inbound to many hosts""" - ... - - @post("/hosts/bulk/set-port", response_class=SetPortToManyHostsResponseDto) - async def set_port_to_hosts( - self, - body: Annotated[SetPortToManyHostsRequestDto, PydanticBody()], - ) -> SetPortToManyHostsResponseDto: - """Set port to many hosts""" + body: Annotated[UpdateManyHostsRequestDto, PydanticBody()], + ) -> UpdateManyHostsResponseDto: + """Update many hosts""" ... diff --git a/remnawave/controllers/nodes.py b/remnawave/controllers/nodes.py index 27d16a3..2ad5b9f 100644 --- a/remnawave/controllers/nodes.py +++ b/remnawave/controllers/nodes.py @@ -19,6 +19,7 @@ from remnawave.models import ( UpdateNodeRequestDto, UpdateNodeResponseDto, RestartAllNodesRequestBodyDto, + RestartNodeRequestBodyDto, ResetNodeTrafficRequestDto, ResetNodeTrafficResponseDto, ProfileModificationRequestDto, @@ -98,6 +99,7 @@ class NodesController(BaseController): async def restart_node( self, uuid: Annotated[str, Path(description="Node UUID")], + body: Annotated[RestartNodeRequestBodyDto | None, PydanticBody()] = None, ) -> RestartNodeResponseDto: """Restart Node""" ... diff --git a/remnawave/controllers/system.py b/remnawave/controllers/system.py index 68edc2e..ab359a3 100644 --- a/remnawave/controllers/system.py +++ b/remnawave/controllers/system.py @@ -7,8 +7,6 @@ from remnawave.models import ( GetNodesMetricsResponseDto, GetRemnawaveHealthResponseDto, GetX25519KeyPairResponseDto, - EncryptHappCryptoLinkRequestDto, - EncryptHappCryptoLinkResponseDto, DebugSrrMatcherRequestDto, DebugSrrMatcherResponseDto, GetMetadataResponseDto, @@ -67,14 +65,6 @@ class SystemController(BaseController): """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, diff --git a/remnawave/controllers/users.py b/remnawave/controllers/users.py index 8933378..7108ae8 100644 --- a/remnawave/controllers/users.py +++ b/remnawave/controllers/users.py @@ -15,6 +15,7 @@ from remnawave.models import ( GetUserByUuidResponseDto, GetUserAccessibleNodesResponseDto, GetUserSubscriptionRequestHistoryResponseDto, + GetUsersStreamResponseDto, TelegramUserResponseDto, EmailUserResponseDto, TagUserResponseDto, @@ -56,13 +57,31 @@ class UsersController(BaseController): Query(default=None, description="Offset for pagination") ] = None, size: Annotated[ - Optional[int], + Optional[int], Query(default=None, description="Page size for pagination") ] = None, ) -> GetAllUsersResponseDto: """Get all users""" ... + @get("/users/stream", response_class=GetUsersStreamResponseDto) + async def get_users_stream( + self, + size: Annotated[ + Optional[int], + Query(default=None, description="Page size, no more than 1000 (default 250)"), + ] = None, + cursor: Annotated[ + Optional[str], + Query( + default=None, + description="Cursor from the previous response (nextCursor). Omit on the first request", + ), + ] = None, + ) -> GetUsersStreamResponseDto: + """Get all users using cursor-based (keyset) pagination""" + ... + @delete("/users/{uuid}", response_class=DeleteUserResponseDto) async def delete_user( self, @@ -75,7 +94,7 @@ class UsersController(BaseController): async def revoke_user_subscription( self, uuid: Annotated[str, Path(description="UUID of the user")], - body: Optional[Annotated[RevokeUserRequestDto, PydanticBody()]] = None, + body: Annotated[Optional[RevokeUserRequestDto], PydanticBody()] = None, ) -> RevokeUserSubscriptionResponseDto: """Revoke User Subscription""" ... diff --git a/remnawave/enums/__init__.py b/remnawave/enums/__init__.py index 55a2cc1..121b647 100644 --- a/remnawave/enums/__init__.py +++ b/remnawave/enums/__init__.py @@ -2,6 +2,8 @@ from .alpn import ALPN from .client_type import ClientType from .error_code import ErrorCode from .fingerprint import Fingerprint +from .mihomo import MihomoIpVersion +from .scopes import Scope from .security_layer import SecurityLayer from .template_type import TemplateType from .users import TrafficLimitStrategy, UserStatus @@ -25,6 +27,8 @@ __all__ = [ "ClientType", "ALPN", "Fingerprint", + "MihomoIpVersion", + "Scope", "SecurityLayer", "TemplateType", "ResponseRuleConditionOperator", diff --git a/remnawave/enums/mihomo.py b/remnawave/enums/mihomo.py new file mode 100644 index 0000000..63cadbb --- /dev/null +++ b/remnawave/enums/mihomo.py @@ -0,0 +1,9 @@ +from enum import StrEnum + + +class MihomoIpVersion(StrEnum): + DUAL = "dual" + IPV4 = "ipv4" + IPV6 = "ipv6" + IPV4_PREFER = "ipv4-prefer" + IPV6_PREFER = "ipv6-prefer" diff --git a/remnawave/enums/scopes.py b/remnawave/enums/scopes.py new file mode 100644 index 0000000..ff163ca --- /dev/null +++ b/remnawave/enums/scopes.py @@ -0,0 +1,279 @@ +from enum import StrEnum + + +class Scope(StrEnum): + """API token scopes available in Remnawave API v2.8.0. + + Mirrors the catalog returned by ``GET /api/tokens/scopes``. Use these members + when creating API tokens, e.g. ``CreateApiTokenRequestDto(scopes=[Scope.USERS_READ])``. + + Coarse scopes per resource: ``:*`` (``*_ALL``), ``:read``, + ``:write``. Fine-grained per-endpoint scopes are also available. + """ + + WILDCARD = "*" + + # Users + USERS_ALL = "users:*" + USERS_READ = "users:read" + USERS_WRITE = "users:write" + USERS_CREATE = "users:create" + USERS_UPDATE = "users:update" + USERS_DELETE = "users:delete" + USERS_LIST = "users:list" + USERS_STREAM = "users:stream" + USERS_LIST_TAGS = "users:list-tags" + USERS_ACCESSIBLE_NODES = "users:accessible-nodes" + USERS_SUBSCRIPTION_REQUEST_HISTORY = "users:subscription-request-history" + USERS_BY_SHORT_UUID = "users:by-short-uuid" + USERS_BY_UUID = "users:by-uuid" + USERS_BY_USERNAME = "users:by-username" + USERS_BY_ID = "users:by-id" + USERS_BY_TELEGRAM_ID = "users:by-telegram-id" + USERS_BY_EMAIL = "users:by-email" + USERS_BY_TAG = "users:by-tag" + USERS_REVOKE_SUBSCRIPTION = "users:revoke-subscription" + USERS_DISABLE = "users:disable" + USERS_ENABLE = "users:enable" + USERS_RESET_TRAFFIC = "users:reset-traffic" + USERS_RESOLVE = "users:resolve" + USERS_BULK_DELETE_BY_STATUS = "users:bulk-delete-by-status" + USERS_BULK_DELETE = "users:bulk-delete" + USERS_BULK_REVOKE_SUBSCRIPTION = "users:bulk-revoke-subscription" + USERS_BULK_RESET_TRAFFIC = "users:bulk-reset-traffic" + USERS_BULK_UPDATE_USERS = "users:bulk-update-users" + USERS_BULK_UPDATE_SQUADS = "users:bulk-update-squads" + USERS_BULK_EXTEND_EXPIRATION_DATE = "users:bulk-extend-expiration-date" + USERS_BULK_ALL_UPDATE_USERS = "users:bulk-all-update-users" + USERS_BULK_ALL_RESET_TRAFFIC = "users:bulk-all-reset-traffic" + USERS_BULK_ALL_EXTEND_EXPIRATION_DATE = "users:bulk-all-extend-expiration-date" + + # Hwid User Devices + HWID_USER_DEVICES_ALL = "hwid-user-devices:*" + HWID_USER_DEVICES_READ = "hwid-user-devices:read" + HWID_USER_DEVICES_WRITE = "hwid-user-devices:write" + HWID_USER_DEVICES_LIST = "hwid-user-devices:list" + HWID_USER_DEVICES_CREATE = "hwid-user-devices:create" + HWID_USER_DEVICES_DELETE = "hwid-user-devices:delete" + HWID_USER_DEVICES_DELETE_ALL = "hwid-user-devices:delete-all" + HWID_USER_DEVICES_STATS = "hwid-user-devices:stats" + HWID_USER_DEVICES_TOP_USERS = "hwid-user-devices:top-users" + HWID_USER_DEVICES_LIST_BY_USER = "hwid-user-devices:list-by-user" + + # Subscriptions + SUBSCRIPTIONS_ALL = "subscriptions:*" + SUBSCRIPTIONS_READ = "subscriptions:read" + SUBSCRIPTIONS_WRITE = "subscriptions:write" + SUBSCRIPTIONS_LIST = "subscriptions:list" + SUBSCRIPTIONS_BY_USERNAME = "subscriptions:by-username" + SUBSCRIPTIONS_BY_SHORT_UUID_PROTECTED = "subscriptions:by-short-uuid-protected" + SUBSCRIPTIONS_BY_UUID = "subscriptions:by-uuid" + SUBSCRIPTIONS_RAW = "subscriptions:raw" + SUBSCRIPTIONS_SUBPAGE_CONFIG = "subscriptions:subpage-config" + SUBSCRIPTIONS_CONNECTION_KEYS = "subscriptions:connection-keys" + + # Nodes + NODES_ALL = "nodes:*" + NODES_READ = "nodes:read" + NODES_WRITE = "nodes:write" + NODES_LIST_TAGS = "nodes:list-tags" + NODES_CREATE = "nodes:create" + NODES_LIST = "nodes:list" + NODES_GET = "nodes:get" + NODES_ENABLE = "nodes:enable" + NODES_DISABLE = "nodes:disable" + NODES_DELETE = "nodes:delete" + NODES_UPDATE = "nodes:update" + NODES_RESTART = "nodes:restart" + NODES_RESET_TRAFFIC = "nodes:reset-traffic" + NODES_RESTART_ALL = "nodes:restart-all" + NODES_REORDER = "nodes:reorder" + NODES_BULK_PROFILE_MODIFICATION = "nodes:bulk-profile-modification" + NODES_BULK_ACTIONS = "nodes:bulk-actions" + NODES_BULK_UPDATE = "nodes:bulk-update" + + # Node Plugins + NODE_PLUGINS_ALL = "node-plugins:*" + NODE_PLUGINS_READ = "node-plugins:read" + NODE_PLUGINS_WRITE = "node-plugins:write" + NODE_PLUGINS_TORRENT_BLOCKER_REPORTS = "node-plugins:torrent-blocker-reports" + NODE_PLUGINS_TORRENT_BLOCKER_STATS = "node-plugins:torrent-blocker-stats" + NODE_PLUGINS_TRUNCATE = "node-plugins:truncate" + NODE_PLUGINS_LIST = "node-plugins:list" + NODE_PLUGINS_GET = "node-plugins:get" + NODE_PLUGINS_UPDATE = "node-plugins:update" + NODE_PLUGINS_DELETE = "node-plugins:delete" + NODE_PLUGINS_CREATE = "node-plugins:create" + NODE_PLUGINS_REORDER = "node-plugins:reorder" + NODE_PLUGINS_CLONE = "node-plugins:clone" + NODE_PLUGINS_EXECUTOR = "node-plugins:executor" + + # Bandwidth Stats + BANDWIDTH_STATS_ALL = "bandwidth-stats:*" + BANDWIDTH_STATS_READ = "bandwidth-stats:read" + BANDWIDTH_STATS_WRITE = "bandwidth-stats:write" + BANDWIDTH_STATS_NODE_USERS_USAGE_LEGACY = "bandwidth-stats:node-users-usage-legacy" + BANDWIDTH_STATS_NODE_USERS_USAGE = "bandwidth-stats:node-users-usage" + BANDWIDTH_STATS_NODES_USERS_USAGE = "bandwidth-stats:nodes-users-usage" + BANDWIDTH_STATS_USER_USAGE_LEGACY = "bandwidth-stats:user-usage-legacy" + BANDWIDTH_STATS_USER_USAGE = "bandwidth-stats:user-usage" + BANDWIDTH_STATS_NODES_USAGE = "bandwidth-stats:nodes-usage" + + # Ip Control + IP_CONTROL_ALL = "ip-control:*" + IP_CONTROL_READ = "ip-control:read" + IP_CONTROL_WRITE = "ip-control:write" + IP_CONTROL_FETCH_IPS = "ip-control:fetch-ips" + IP_CONTROL_FETCH_IPS_RESULT = "ip-control:fetch-ips-result" + IP_CONTROL_DROP_CONNECTIONS = "ip-control:drop-connections" + IP_CONTROL_FETCH_USERS_IPS = "ip-control:fetch-users-ips" + IP_CONTROL_FETCH_USERS_IPS_RESULT = "ip-control:fetch-users-ips-result" + + # Config Profiles + CONFIG_PROFILES_ALL = "config-profiles:*" + CONFIG_PROFILES_READ = "config-profiles:read" + CONFIG_PROFILES_WRITE = "config-profiles:write" + CONFIG_PROFILES_LIST = "config-profiles:list" + CONFIG_PROFILES_LIST_INBOUNDS = "config-profiles:list-inbounds" + CONFIG_PROFILES_LIST_PROFILE_INBOUNDS = "config-profiles:list-profile-inbounds" + CONFIG_PROFILES_GET = "config-profiles:get" + CONFIG_PROFILES_GET_COMPUTED = "config-profiles:get-computed" + CONFIG_PROFILES_DELETE = "config-profiles:delete" + CONFIG_PROFILES_CREATE = "config-profiles:create" + CONFIG_PROFILES_UPDATE = "config-profiles:update" + CONFIG_PROFILES_REORDER = "config-profiles:reorder" + + # Internal Squads + INTERNAL_SQUADS_ALL = "internal-squads:*" + INTERNAL_SQUADS_READ = "internal-squads:read" + INTERNAL_SQUADS_WRITE = "internal-squads:write" + INTERNAL_SQUADS_LIST = "internal-squads:list" + INTERNAL_SQUADS_GET = "internal-squads:get" + INTERNAL_SQUADS_CREATE = "internal-squads:create" + INTERNAL_SQUADS_ACCESSIBLE_NODES = "internal-squads:accessible-nodes" + INTERNAL_SQUADS_UPDATE = "internal-squads:update" + INTERNAL_SQUADS_DELETE = "internal-squads:delete" + INTERNAL_SQUADS_ADD_USERS = "internal-squads:add-users" + INTERNAL_SQUADS_REMOVE_USERS = "internal-squads:remove-users" + INTERNAL_SQUADS_REORDER = "internal-squads:reorder" + + # External Squads + EXTERNAL_SQUADS_ALL = "external-squads:*" + EXTERNAL_SQUADS_READ = "external-squads:read" + EXTERNAL_SQUADS_WRITE = "external-squads:write" + EXTERNAL_SQUADS_LIST = "external-squads:list" + EXTERNAL_SQUADS_GET = "external-squads:get" + EXTERNAL_SQUADS_CREATE = "external-squads:create" + EXTERNAL_SQUADS_UPDATE = "external-squads:update" + EXTERNAL_SQUADS_DELETE = "external-squads:delete" + EXTERNAL_SQUADS_ADD_USERS = "external-squads:add-users" + EXTERNAL_SQUADS_REMOVE_USERS = "external-squads:remove-users" + EXTERNAL_SQUADS_REORDER = "external-squads:reorder" + + # Hosts + HOSTS_ALL = "hosts:*" + HOSTS_READ = "hosts:read" + HOSTS_WRITE = "hosts:write" + HOSTS_LIST_TAGS = "hosts:list-tags" + HOSTS_CREATE = "hosts:create" + HOSTS_UPDATE = "hosts:update" + HOSTS_LIST = "hosts:list" + HOSTS_GET = "hosts:get" + HOSTS_REORDER = "hosts:reorder" + HOSTS_DELETE = "hosts:delete" + HOSTS_BULK_DELETE = "hosts:bulk-delete" + HOSTS_BULK_DISABLE = "hosts:bulk-disable" + HOSTS_BULK_ENABLE = "hosts:bulk-enable" + HOSTS_BULK_UPDATE = "hosts:bulk-update" + + # Subscription Template + SUBSCRIPTION_TEMPLATE_ALL = "subscription-template:*" + SUBSCRIPTION_TEMPLATE_READ = "subscription-template:read" + SUBSCRIPTION_TEMPLATE_WRITE = "subscription-template:write" + SUBSCRIPTION_TEMPLATE_LIST = "subscription-template:list" + SUBSCRIPTION_TEMPLATE_GET = "subscription-template:get" + SUBSCRIPTION_TEMPLATE_UPDATE = "subscription-template:update" + SUBSCRIPTION_TEMPLATE_DELETE = "subscription-template:delete" + SUBSCRIPTION_TEMPLATE_CREATE = "subscription-template:create" + SUBSCRIPTION_TEMPLATE_REORDER = "subscription-template:reorder" + + # Subscription Settings + SUBSCRIPTION_SETTINGS_ALL = "subscription-settings:*" + SUBSCRIPTION_SETTINGS_READ = "subscription-settings:read" + SUBSCRIPTION_SETTINGS_WRITE = "subscription-settings:write" + SUBSCRIPTION_SETTINGS_GET = "subscription-settings:get" + SUBSCRIPTION_SETTINGS_UPDATE = "subscription-settings:update" + + # Infra Billing + INFRA_BILLING_ALL = "infra-billing:*" + INFRA_BILLING_READ = "infra-billing:read" + INFRA_BILLING_WRITE = "infra-billing:write" + INFRA_BILLING_LIST_PROVIDERS = "infra-billing:list-providers" + INFRA_BILLING_GET_PROVIDER = "infra-billing:get-provider" + INFRA_BILLING_DELETE_PROVIDER = "infra-billing:delete-provider" + INFRA_BILLING_CREATE_PROVIDER = "infra-billing:create-provider" + INFRA_BILLING_UPDATE_PROVIDER = "infra-billing:update-provider" + INFRA_BILLING_CREATE_BILL_RECORD = "infra-billing:create-bill-record" + INFRA_BILLING_LIST_BILL_RECORDS = "infra-billing:list-bill-records" + INFRA_BILLING_DELETE_BILL_RECORD = "infra-billing:delete-bill-record" + INFRA_BILLING_LIST_BILLING_NODES = "infra-billing:list-billing-nodes" + INFRA_BILLING_UPDATE_BILLING_NODE = "infra-billing:update-billing-node" + INFRA_BILLING_CREATE_BILLING_NODE = "infra-billing:create-billing-node" + INFRA_BILLING_DELETE_BILLING_NODE = "infra-billing:delete-billing-node" + + # System + SYSTEM_ALL = "system:*" + SYSTEM_READ = "system:read" + SYSTEM_WRITE = "system:write" + SYSTEM_METADATA = "system:metadata" + SYSTEM_STATS = "system:stats" + SYSTEM_BANDWIDTH_STATS = "system:bandwidth-stats" + SYSTEM_NODES_STATISTICS = "system:nodes-statistics" + SYSTEM_REMNAWAVE_HEALTH = "system:remnawave-health" + SYSTEM_NODES_METRICS = "system:nodes-metrics" + SYSTEM_GENERATE_X25519 = "system:generate-x25519" + SYSTEM_TEST_SRR_MATCHER = "system:test-srr-matcher" + SYSTEM_RECAP = "system:recap" + + # Keygen + KEYGEN_ALL = "keygen:*" + KEYGEN_READ = "keygen:read" + KEYGEN_WRITE = "keygen:write" + KEYGEN_GET = "keygen:get" + + # Subscription Request History + SUBSCRIPTION_REQUEST_HISTORY_ALL = "subscription-request-history:*" + SUBSCRIPTION_REQUEST_HISTORY_READ = "subscription-request-history:read" + SUBSCRIPTION_REQUEST_HISTORY_WRITE = "subscription-request-history:write" + SUBSCRIPTION_REQUEST_HISTORY_LIST = "subscription-request-history:list" + SUBSCRIPTION_REQUEST_HISTORY_STATS = "subscription-request-history:stats" + + # Snippets + SNIPPETS_ALL = "snippets:*" + SNIPPETS_READ = "snippets:read" + SNIPPETS_WRITE = "snippets:write" + SNIPPETS_LIST = "snippets:list" + SNIPPETS_DELETE = "snippets:delete" + SNIPPETS_CREATE = "snippets:create" + SNIPPETS_UPDATE = "snippets:update" + + # Subscription Page Configs + SUBSCRIPTION_PAGE_CONFIGS_ALL = "subscription-page-configs:*" + SUBSCRIPTION_PAGE_CONFIGS_READ = "subscription-page-configs:read" + SUBSCRIPTION_PAGE_CONFIGS_WRITE = "subscription-page-configs:write" + SUBSCRIPTION_PAGE_CONFIGS_LIST = "subscription-page-configs:list" + SUBSCRIPTION_PAGE_CONFIGS_GET = "subscription-page-configs:get" + SUBSCRIPTION_PAGE_CONFIGS_UPDATE = "subscription-page-configs:update" + SUBSCRIPTION_PAGE_CONFIGS_DELETE = "subscription-page-configs:delete" + SUBSCRIPTION_PAGE_CONFIGS_CREATE = "subscription-page-configs:create" + SUBSCRIPTION_PAGE_CONFIGS_REORDER = "subscription-page-configs:reorder" + SUBSCRIPTION_PAGE_CONFIGS_CLONE = "subscription-page-configs:clone" + + # Metadata + METADATA_ALL = "metadata:*" + METADATA_READ = "metadata:read" + METADATA_WRITE = "metadata:write" + METADATA_GET_USER = "metadata:get-user" + METADATA_UPSERT_USER = "metadata:upsert-user" + METADATA_GET_NODE = "metadata:get-node" + METADATA_UPSERT_NODE = "metadata:upsert-node" diff --git a/remnawave/enums/webhook.py b/remnawave/enums/webhook.py index 2cb7606..0866091 100644 --- a/remnawave/enums/webhook.py +++ b/remnawave/enums/webhook.py @@ -23,12 +23,10 @@ TUserEvents = Literal[ "user.limited", "user.expired", "user.traffic_reset", - "user.expires_in_72_hours", - "user.expires_in_48_hours", - "user.expires_in_24_hours", - "user.expired_24_hours_ago", "user.first_connected", "user.bandwidth_usage_threshold_reached", + "user.not_connected", + "user.expiration", ] TServiceEvents = Literal[ @@ -36,6 +34,8 @@ TServiceEvents = Literal[ "service.login_attempt_failed", "service.login_attempt_success", "service.subpage_config_changed", + "service.api_token_created", + "service.api_token_deleted", ] TErrorsEvents = Literal[ diff --git a/remnawave/models/__init__.py b/remnawave/models/__init__.py index 07c49b5..154e481 100644 --- a/remnawave/models/__init__.py +++ b/remnawave/models/__init__.py @@ -1,8 +1,13 @@ from .api_tokens_management import ( + ApiTokenDto, + ApiTokenScopeEndpointDto, + ApiTokenScopeResourceDto, CreateApiTokenRequestDto, CreateApiTokenResponseDto, DeleteApiTokenResponseDto, + DocsInfoDto, FindAllApiTokensResponseDto, + GetApiTokenScopesResponseDto, ) from .auth import ( GetStatusResponseDto, @@ -36,6 +41,8 @@ from .bandwidthstats import ( GetStatsNodesRealtimeUsageResponseDto, GetStatsNodesUsageResponseDto, GetStatsNodeUsersUsageResponseDto, + GetStatsNodesUsersUsageRequestDto, + GetStatsNodesUsersUsageResponseDto, GetStatsUserUsageResponseDto, # Data Models @@ -44,9 +51,11 @@ from .bandwidthstats import ( NodeRealtimeUsageItem, TopNodeItem, TopUserItem, + TopNodesUserItem, NodeSeriesItem, StatsNodesUsageData, StatsNodeUsersUsageData, + StatsNodesUsersUsageData, StatsUserUsageData, ) from .config_profiles import ( @@ -88,10 +97,8 @@ from .hosts_bulk_actions import ( BulkDeleteHostsResponseDto, BulkDisableHostsResponseDto, BulkEnableHostsResponseDto, - SetInboundToManyHostsRequestDto, - SetInboundToManyHostsResponseDto, - SetPortToManyHostsResponseDto, - SetPortToManyHostsRequestDto + UpdateManyHostsRequestDto, + UpdateManyHostsResponseDto, ) from .hwid import ( CreateHWIDUser, # Legacy alias @@ -199,6 +206,7 @@ from .nodes import ( UpdateNodeResponseDto, RestartAllNodesRequestDto, # Legacy alias, RestartAllNodesRequestBodyDto, + RestartNodeRequestBodyDto, ResetNodeTrafficRequestDto, ResetNodeTrafficResponseDto, ProfileModificationRequestDto, @@ -281,8 +289,6 @@ from .system import ( X25519KeyPair, DebugSrrMatcherRequestDto, DebugSrrMatcherResponseDto, - EncryptHappCryptoLinkRequestDto, - EncryptHappCryptoLinkResponseDto, GetMetadataResponseDto, GetRecapResponseDto, RecapThisMonth, @@ -313,6 +319,8 @@ from .users import ( GetAllUsersResponseDto, GetAllTagsResponseDto, GetUserSubscriptionRequestHistoryResponseDto, + GetUsersStreamResponseDto, + UsersStreamData, # Response DTOs - Arrays (RootModel) TelegramUserResponseDto, @@ -589,6 +597,7 @@ __all__ = [ "NodeConfigProfileRequestDto", "RestartAllNodesRequestDto", # Legacy alias "RestartAllNodesRequestBodyDto", + "RestartNodeRequestBodyDto", "ResetNodeTrafficRequestDto", "ResetNodeTrafficResponseDto", "ProfileModificationRequestDto", @@ -689,8 +698,6 @@ __all__ = [ "X25519KeyPair", "DebugSrrMatcherRequestDto", "DebugSrrMatcherResponseDto", - "EncryptHappCryptoLinkRequestDto", - "EncryptHappCryptoLinkResponseDto", "GetMetadataResponseDto", "GetRecapResponseDto", "RecapThisMonth", @@ -729,21 +736,30 @@ __all__ = [ "GetStatsNodesRealtimeUsageResponseDto", "GetStatsNodesUsageResponseDto", "GetStatsNodeUsersUsageResponseDto", + "GetStatsNodesUsersUsageRequestDto", + "GetStatsNodesUsersUsageResponseDto", "GetStatsUserUsageResponseDto", "LegacyUserUsageItem", "LegacyNodeUserUsageItem", "NodeRealtimeUsageItem", "TopNodeItem", "TopUserItem", + "TopNodesUserItem", "NodeSeriesItem", "StatsNodesUsageData", "StatsNodeUsersUsageData", + "StatsNodesUsersUsageData", "StatsUserUsageData", # API Tokens models + "ApiTokenDto", + "ApiTokenScopeEndpointDto", + "ApiTokenScopeResourceDto", "CreateApiTokenRequestDto", "CreateApiTokenResponseDto", "DeleteApiTokenResponseDto", + "DocsInfoDto", "FindAllApiTokensResponseDto", + "GetApiTokenScopesResponseDto", # Inbound bulk actions models "AddInboundToNodesResponseDto", "AddInboundToUsersResponseDto", @@ -753,10 +769,8 @@ __all__ = [ "BulkDeleteHostsResponseDto", "BulkDisableHostsResponseDto", "BulkEnableHostsResponseDto", - "SetInboundToManyHostsRequestDto", - "SetInboundToManyHostsResponseDto", - "SetPortToManyHostsResponseDto", - "SetPortToManyHostsRequestDto", + "UpdateManyHostsRequestDto", + "UpdateManyHostsResponseDto", # Users models "CreateUserRequestDto", "UpdateUserRequestDto", @@ -777,6 +791,8 @@ __all__ = [ "GetAllUsersResponseDto", "GetAllTagsResponseDto", "GetUserSubscriptionRequestHistoryResponseDto", + "GetUsersStreamResponseDto", + "UsersStreamData", "TelegramUserResponseDto", "EmailUserResponseDto", "TagUserResponseDto", diff --git a/remnawave/models/api_tokens_management.py b/remnawave/models/api_tokens_management.py index 8a6108d..245fe33 100644 --- a/remnawave/models/api_tokens_management.py +++ b/remnawave/models/api_tokens_management.py @@ -1,16 +1,37 @@ from datetime import datetime -from typing import List, Optional +from typing import Annotated, List, Literal, Optional +from uuid import UUID -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, Field, StringConstraints + +from remnawave.enums import Scope class CreateApiTokenRequestDto(BaseModel): - token_name: str = Field(serialization_alias="tokenName") + name: Annotated[str, StringConstraints(min_length=2, max_length=30)] = Field( + serialization_alias="name" + ) + expires_in_days: float = Field(serialization_alias="expiresInDays", ge=1) + scopes: List[str] = Field( + default_factory=lambda: [Scope.WILDCARD], + description='API token scopes. Pass :class:`remnawave.enums.Scope` members (or raw strings). Defaults to ["*"] (full access). See GET /api/tokens/scopes for the catalog.', + ) + + def __init__(self, **data): + # Backward compatibility: `token_name` was renamed to `name` in v2.8.0 + if "token_name" in data and "name" not in data: + data["name"] = data.pop("token_name") + super().__init__(**data) class CreateApiTokenResponseData(BaseModel): + uuid: UUID + name: str + expire_at: datetime = Field(alias="expireAt") + scopes: List[str] + created_at: datetime = Field(alias="createdAt") + updated_at: datetime = Field(alias="updatedAt") token: str - uuid: str class CreateApiTokenResponseDto(CreateApiTokenResponseData): @@ -22,23 +43,62 @@ class DeleteApiTokenResponseDto(BaseModel): class ApiTokenDto(BaseModel): - uuid: str - token: str - token_name: str = Field(..., alias="tokenName") + uuid: UUID + name: str + expire_at: datetime = Field(..., alias="expireAt") + scopes: List[str] = Field(default_factory=list) created_at: datetime = Field(..., alias="createdAt") updated_at: datetime = Field(..., alias="updatedAt") + @property + def token_name(self) -> str: + """Backward compatibility property (renamed to `name` in v2.8.0)""" + return self.name + class DocsInfoDto(BaseModel): - is_docs_enabled: bool = Field(..., alias="isDocsEnabled") + enabled: bool = Field(..., alias="enabled") scalar_path: Optional[str] = Field(None, alias="scalarPath") swagger_path: Optional[str] = Field(None, alias="swaggerPath") + @property + def is_docs_enabled(self) -> bool: + """Backward compatibility property (renamed to `enabled` in v2.8.0)""" + return self.enabled + class FindAllApiTokensResponseData(BaseModel): - api_keys: List[ApiTokenDto] = Field(..., alias="apiKeys") + tokens: List[ApiTokenDto] = Field(..., alias="tokens") docs: DocsInfoDto + @property + def api_keys(self) -> List[ApiTokenDto]: + """Backward compatibility property (renamed to `tokens` in v2.8.0)""" + return self.tokens + class FindAllApiTokensResponseDto(FindAllApiTokensResponseData): pass + + +class ApiTokenScopeEndpointDto(BaseModel): + key: str + kind: Literal["read", "write"] + method: str + path: str + description: str + + +class ApiTokenScopeResourceDto(BaseModel): + resource: str + resource_scopes: List[str] = Field(..., alias="resourceScopes") + endpoints: List[ApiTokenScopeEndpointDto] + + +class GetApiTokenScopesResponseData(BaseModel): + wildcard: str + resources: List[ApiTokenScopeResourceDto] + + +class GetApiTokenScopesResponseDto(GetApiTokenScopesResponseData): + pass diff --git a/remnawave/models/bandwidthstats.py b/remnawave/models/bandwidthstats.py index 981588f..d6f2c9a 100644 --- a/remnawave/models/bandwidthstats.py +++ b/remnawave/models/bandwidthstats.py @@ -277,6 +277,34 @@ class GetStatsNodeUsersUsageResponseDto(RootModel[StatsNodeUsersUsageData]): return self.root +# Stats Nodes Users Usage by Nodes UUIDs (POST /bandwidth-stats/nodes/users) + +class GetStatsNodesUsersUsageRequestDto(BaseModel): + """Request for nodes users usage by nodes UUIDs""" + nodes_uuids: List[UUID] = Field(serialization_alias="nodesUuids", min_length=1) + + +class TopNodesUserItem(BaseModel): + """Top user item for nodes users usage""" + color: str + username: str + total: float + + +class StatsNodesUsersUsageData(BaseModel): + """Stats nodes users usage data""" + categories: List[str] + sparkline_data: List[float] = Field(alias="sparklineData") + top_users: List[TopNodesUserItem] = Field(alias="topUsers") + + +class GetStatsNodesUsersUsageResponseDto(RootModel[StatsNodesUsersUsageData]): + """Response for stats nodes users usage by nodes UUIDs""" + @property + def response(self) -> StatsNodesUsersUsageData: + return self.root + + # Stats User Usage (with charts) class StatsUserUsageData(BaseModel): diff --git a/remnawave/models/hosts.py b/remnawave/models/hosts.py index c732985..5951e5c 100644 --- a/remnawave/models/hosts.py +++ b/remnawave/models/hosts.py @@ -3,7 +3,10 @@ from uuid import UUID from pydantic import BaseModel, Field, StringConstraints, RootModel -from remnawave.enums import ALPN, Fingerprint, SecurityLayer, SubscriptionType +from remnawave.enums import ALPN, MihomoIpVersion, SecurityLayer, SubscriptionType + +# Tag for a single host tag entry: uppercase alphanumeric, underscores and colons, max 36 chars +HostTag = Annotated[str, StringConstraints(max_length=36, pattern=r"^[A-Z0-9_:]+$")] class ReorderHostItem(BaseModel): @@ -35,21 +38,24 @@ class UpdateHostRequestDto(BaseModel): sni: Optional[str] = None host: Optional[str] = None alpn: Optional[ALPN] = None - fingerprint: Optional[Fingerprint] = None - allow_insecure: Optional[bool] = Field(None, serialization_alias="allowInsecure") + fingerprint: Optional[str] = None 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) - tag: Optional[Annotated[str, StringConstraints(max_length=32, pattern=r"^[A-Z0-9_:]+$")]] = None + tags: Optional[List[HostTag]] = Field(None, serialization_alias="tags", max_length=10) is_hidden: Optional[bool] = Field(None, serialization_alias="isHidden") override_sni_from_address: Optional[bool] = Field(None, serialization_alias="overrideSniFromAddress") keep_blank_sni: Optional[bool] = Field(None, serialization_alias="keepSniBlank") vless_route_id: Optional[int] = Field(None, serialization_alias="vlessRouteId", ge=0, le=65535) + pinned_peer_cert_sha256: Optional[str] = Field(None, serialization_alias="pinnedPeerCertSha256") + verify_peer_cert_by_name: Optional[str] = Field(None, serialization_alias="verifyPeerCertByName") 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") + mihomo_ip_version: Optional[MihomoIpVersion] = Field(None, serialization_alias="mihomoIpVersion") + xhttp_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") + final_mask: Optional[Any] = Field(None, serialization_alias="finalMask") 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") @@ -59,6 +65,28 @@ class UpdateHostRequestDto(BaseModel): description="Subscription types from which this host will be excluded.", ) + def __init__(self, **data): + # Backward compatibility: `tag` (single value) was replaced by `tags` (list) in v2.8.0 + if "tag" in data and "tags" not in data: + tag = data.pop("tag") + data["tags"] = [tag] if tag is not None else None + # Backward compatibility: `allow_insecure` was removed in v2.8.0 (use security_layer instead) + data.pop("allow_insecure", None) + # Backward compatibility: `xHttpExtraParams` alias was renamed to `xhttpExtraParams` + if "x_http_extra_params" in data and "xhttp_extra_params" not in data: + data["xhttp_extra_params"] = data.pop("x_http_extra_params") + super().__init__(**data) + + @property + def x_http_extra_params(self) -> Optional[Dict[str, Any]]: + """Backward compatibility property (renamed to xhttp_extra_params in v2.8.0)""" + return self.xhttp_extra_params + + @property + def tag(self) -> Optional[str]: + """Backward compatibility property (replaced by `tags` in v2.8.0)""" + return self.tags[0] if self.tags else None + @property def inbound_uuid(self) -> Optional[UUID]: return self.inbound.config_profile_inbound_uuid if self.inbound else None @@ -75,22 +103,25 @@ class HostResponseDto(BaseModel): host: str | None = Field(alias="host") alpn: str | None = Field(alias="alpn") fingerprint: str | None = Field(alias="fingerprint") - x_http_extra_params: Dict[str, Any] | None = Field(alias="xHttpExtraParams") + xhttp_extra_params: Dict[str, Any] | None = Field(None, alias="xhttpExtraParams") mux_params: Dict[str, Any] | None = Field(alias="muxParams") sockopt_params: Dict[str, Any] | None = Field(alias="sockoptParams") + final_mask: Any | None = Field(None, alias="finalMask") inbound: HostInboundData server_description: str | None = Field(alias="serverDescription") - tag: str | None = Field(alias="tag") + tags: List[str] = Field(default_factory=list, alias="tags") vless_route_id: int | None = Field(alias="vlessRouteId") + pinned_peer_cert_sha256: str | None = Field(None, alias="pinnedPeerCertSha256") + verify_peer_cert_by_name: str | None = Field(None, alias="verifyPeerCertByName") shuffle_host: bool = Field(alias="shuffleHost") mihomo_x25519: bool = Field(alias="mihomoX25519") + mihomo_ip_version: str | None = Field(None, alias="mihomoIpVersion") nodes: List[UUID] 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") keep_blank_sni: bool = Field(False, alias="keepSniBlank") - allow_insecure: bool = Field(False, alias="allowInsecure") xray_json_template_uuid: UUID | None = Field(alias="xrayJsonTemplateUuid") excluded_internal_squads: List[UUID] = Field(default_factory=list, alias="excludedInternalSquads") exclude_from_subscription_types: List[SubscriptionType] = Field( @@ -103,6 +134,21 @@ class HostResponseDto(BaseModel): def inbound_uuid(self) -> Optional[UUID]: return self.inbound.config_profile_inbound_uuid + @property + def x_http_extra_params(self) -> Dict[str, Any] | None: + """Backward compatibility property (renamed to xhttp_extra_params in v2.8.0)""" + return self.xhttp_extra_params + + @property + def tag(self) -> str | None: + """Backward compatibility property (replaced by `tags` in v2.8.0)""" + return self.tags[0] if self.tags else None + + @property + def allow_insecure(self) -> bool: + """Backward compatibility property (removed in v2.8.0, derived from security_layer)""" + return self.security_layer == SecurityLayer.NONE + class CreateHostRequestDto(BaseModel): inbound: CreateHostInboundData @@ -113,17 +159,20 @@ class CreateHostRequestDto(BaseModel): sni: Optional[str] = None host: Optional[str] = None alpn: Optional[ALPN] = None - fingerprint: Optional[Fingerprint] = None - x_http_extra_params: Optional[Dict[str, Any]] = Field(None, serialization_alias="xHttpExtraParams") + fingerprint: Optional[str] = None + xhttp_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") + final_mask: Optional[Any] = Field(None, serialization_alias="finalMask") 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 + tags: Optional[List[HostTag]] = Field(None, serialization_alias="tags", max_length=10) vless_route_id: Optional[int] = Field(None, serialization_alias="vlessRouteId", ge=0, le=65535) + pinned_peer_cert_sha256: Optional[str] = Field(None, serialization_alias="pinnedPeerCertSha256") + verify_peer_cert_by_name: Optional[str] = Field(None, serialization_alias="verifyPeerCertByName") shuffle_host: bool = Field(False, serialization_alias="shuffleHost") mihomo_x25519: bool = Field(False, serialization_alias="mihomoX25519") + mihomo_ip_version: Optional[MihomoIpVersion] = Field(None, serialization_alias="mihomoIpVersion") 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") @@ -141,6 +190,16 @@ class CreateHostRequestDto(BaseModel): def inbound_uuid(self) -> Optional[UUID]: return self.inbound.config_profile_inbound_uuid + @property + def x_http_extra_params(self) -> Optional[Dict[str, Any]]: + """Backward compatibility property (renamed to xhttp_extra_params in v2.8.0)""" + return self.xhttp_extra_params + + @property + def tag(self) -> Optional[str]: + """Backward compatibility property (replaced by `tags` in v2.8.0)""" + return self.tags[0] if self.tags else None + def __init__( self, inbound_uuid: Optional[UUID] = None, @@ -157,6 +216,17 @@ class CreateHostRequestDto(BaseModel): or UUID("107541f1-ae1a-4e2d-9dec-7297557b5125"), config_profile_inbound_uuid=inbound_uuid, ) + + # Backward compatibility: `tag` (single value) was replaced by `tags` (list) in v2.8.0 + if "tag" in data and "tags" not in data: + tag = data.pop("tag") + data["tags"] = [tag] if tag is not None else None + # Backward compatibility: `allow_insecure` was removed in v2.8.0 (use security_layer instead) + data.pop("allow_insecure", None) + # Backward compatibility: `xHttpExtraParams` alias was renamed to `xhttpExtraParams` + if "x_http_extra_params" in data and "xhttp_extra_params" not in data: + data["xhttp_extra_params"] = data.pop("x_http_extra_params") + super().__init__(**data) diff --git a/remnawave/models/hosts_bulk_actions.py b/remnawave/models/hosts_bulk_actions.py index de043c8..9d20e48 100644 --- a/remnawave/models/hosts_bulk_actions.py +++ b/remnawave/models/hosts_bulk_actions.py @@ -1,36 +1,80 @@ -from typing import List +from typing import Annotated, Any, Dict, List, Optional from uuid import UUID -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, RootModel, StringConstraints +from remnawave.enums import ALPN, MihomoIpVersion, SecurityLayer, SubscriptionType from remnawave.models import HostResponseDto +from remnawave.models.hosts import CreateHostInboundData, HostTag -class SetInboundToManyHostsRequestDto(BaseModel): - uuids: List[UUID] - config_profile_uuid: UUID = Field(serialization_alias="configProfileUuid") - config_profile_inbound_uuid: UUID = Field( - serialization_alias="configProfileInboundUuid" +class _HostListResponse(RootModel[List[HostResponseDto]]): + """Base for bulk host responses that return a plain list of hosts.""" + root: List[HostResponseDto] + + def __iter__(self): + return iter(self.root) + + def __getitem__(self, item): + return self.root[item] + + def __bool__(self): + return bool(self.root) + + def __len__(self): + return len(self.root) + + +class BulkDeleteHostsResponseDto(_HostListResponse): + pass + + +class BulkDisableHostsResponseDto(_HostListResponse): + pass + + +class BulkEnableHostsResponseDto(_HostListResponse): + pass + + +class UpdateManyHostsRequestDto(BaseModel): + """Request to update many hosts at once (PATCH /hosts/bulk/update).""" + uuids: List[UUID] = Field(min_length=1) + inbound: Optional[CreateHostInboundData] = None + remark: Annotated[Optional[str], StringConstraints(max_length=40)] = None + address: Optional[str] = None + port: Optional[int] = None + path: Optional[str] = None + sni: Optional[str] = None + host: Optional[str] = None + alpn: Optional[ALPN] = None + fingerprint: Optional[str] = None + 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) + tags: Optional[List[HostTag]] = Field(None, serialization_alias="tags", max_length=10) + is_hidden: Optional[bool] = Field(None, serialization_alias="isHidden") + override_sni_from_address: Optional[bool] = Field(None, serialization_alias="overrideSniFromAddress") + keep_blank_sni: Optional[bool] = Field(None, serialization_alias="keepSniBlank") + vless_route_id: Optional[int] = Field(None, serialization_alias="vlessRouteId", ge=0, le=65535) + pinned_peer_cert_sha256: Optional[str] = Field(None, serialization_alias="pinnedPeerCertSha256") + verify_peer_cert_by_name: Optional[str] = Field(None, serialization_alias="verifyPeerCertByName") + shuffle_host: Optional[bool] = Field(None, serialization_alias="shuffleHost") + mihomo_x25519: Optional[bool] = Field(None, serialization_alias="mihomoX25519") + mihomo_ip_version: Optional[MihomoIpVersion] = Field(None, serialization_alias="mihomoIpVersion") + xhttp_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") + final_mask: Optional[Any] = Field(None, serialization_alias="finalMask") + 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") + exclude_from_subscription_types: Optional[List[SubscriptionType]] = Field( + None, + serialization_alias="excludeFromSubscriptionTypes", + description="Subscription types from which the hosts will be excluded.", ) -class SetPortToManyHostsRequestDto(BaseModel): - uuids: List[UUID] - port: int = Field(ge=1, le=65535) -class BulkDeleteHostsResponseDto(List[HostResponseDto]): - pass - - -class BulkDisableHostsResponseDto(List[HostResponseDto]): - pass - - -class BulkEnableHostsResponseDto(List[HostResponseDto]): - pass - - -class SetInboundToManyHostsResponseDto(List[HostResponseDto]): - pass - -class SetPortToManyHostsResponseDto(List[HostResponseDto]): +class UpdateManyHostsResponseDto(_HostListResponse): pass diff --git a/remnawave/models/hwid.py b/remnawave/models/hwid.py index 1db9551..8ff3ee1 100644 --- a/remnawave/models/hwid.py +++ b/remnawave/models/hwid.py @@ -12,6 +12,7 @@ class CreateUserHwidDeviceRequestDto(BaseModel): os_version: Optional[str] = Field(None, serialization_alias="osVersion") device_model: Optional[str] = Field(None, serialization_alias="deviceModel") user_agent: Optional[str] = Field(None, serialization_alias="userAgent") + request_ip: Optional[str] = Field(None, serialization_alias="requestIp") class DeleteUserHwidDeviceRequestDto(BaseModel): diff --git a/remnawave/models/infra_billing.py b/remnawave/models/infra_billing.py index 6f3316d..8cc1ab9 100644 --- a/remnawave/models/infra_billing.py +++ b/remnawave/models/infra_billing.py @@ -146,9 +146,10 @@ class DeleteInfraBillingHistoryRecordByUuidResponseDto(BaseModel): # Billing Nodes models class CreateInfraBillingNodeRequestDto(BaseModel): - node_uuid: UUID = Field(serialization_alias="nodeUuid") provider_uuid: UUID = Field(serialization_alias="providerUuid") - next_billing_at: Optional[datetime] = Field(None, serialization_alias="nextBillingAt") + node_uuid: Optional[UUID] = Field(None, serialization_alias="nodeUuid") + name: Optional[str] = Field(None, serialization_alias="name", min_length=1, max_length=255) + next_billing_at: datetime = Field(serialization_alias="nextBillingAt") # ИСПРАВЛЕНО: API возвращает список всех billing nodes после создания, а не один созданный diff --git a/remnawave/models/nodes.py b/remnawave/models/nodes.py index 15021f9..95259fd 100644 --- a/remnawave/models/nodes.py +++ b/remnawave/models/nodes.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import Annotated, List, Optional, Union, Literal from uuid import UUID -from pydantic import BaseModel, Field, StringConstraints, RootModel +from pydantic import BaseModel, ConfigDict, Field, StringConstraints, RootModel from remnawave.models.internal_squads import InboundsDto @@ -61,7 +61,7 @@ class CreateNodeRequestDto(BaseModel): False, serialization_alias="isTrafficTrackingActive", ) - traffic_limit_bytes: Optional[int] = Field( + traffic_limit_bytes: Optional[float] = Field( None, serialization_alias="trafficLimitBytes", ge=0 ) notify_percent: Optional[int] = Field( @@ -74,11 +74,20 @@ class CreateNodeRequestDto(BaseModel): None, serialization_alias="excludedInbounds" ) country_code: Annotated[Optional[str], StringConstraints(max_length=2)] = Field( - "XX", + "XX", serialization_alias="countryCode" ) consumption_multiplier: Optional[float] = Field( - None, serialization_alias="consumptionMultiplier", ge=0.1 + None, serialization_alias="consumptionMultiplier", ge=0, le=100 + ) + node_consumption_multiplier: Optional[float] = Field( + None, serialization_alias="nodeConsumptionMultiplier", ge=0, le=100 + ) + note: Optional[str] = Field(None, serialization_alias="note", max_length=255) + proxy_url: Optional[str] = Field( + None, + serialization_alias="proxyUrl", + pattern=r"^socks5://(?:[^:@/\s]+(?::[^@/\s]*)?@)?[^:@/\s]+:\d{1,5}$", ) config_profile: NodeConfigProfileRequestDto = Field( serialization_alias="configProfile" @@ -118,7 +127,16 @@ class UpdateNodeRequestDto(BaseModel): None, serialization_alias="countryCode" ) consumption_multiplier: Optional[float] = Field( - None, serialization_alias="consumptionMultiplier", ge=0.1 + None, serialization_alias="consumptionMultiplier", ge=0, le=100 + ) + node_consumption_multiplier: Optional[float] = Field( + None, serialization_alias="nodeConsumptionMultiplier", ge=0, le=100 + ) + note: Optional[str] = Field(None, serialization_alias="note", max_length=255) + proxy_url: Optional[str] = Field( + None, + serialization_alias="proxyUrl", + pattern=r"^socks5://(?:[^:@/\s]+(?::[^@/\s]*)?@)?[^:@/\s]+:\d{1,5}$", ) config_profile: Optional[NodeConfigProfileRequestDto] = Field( None, serialization_alias="configProfile" @@ -160,6 +178,9 @@ class NodeResponseDto(BaseModel): view_position: int = Field(alias="viewPosition") country_code: str = Field(alias="countryCode") consumption_multiplier: float = Field(alias="consumptionMultiplier") + node_consumption_multiplier: Optional[float] = Field(None, alias="nodeConsumptionMultiplier") + note: Optional[str] = Field(None, alias="note") + proxy_url: Optional[str] = Field(None, alias="proxyUrl") cpu_count: Optional[int] = Field(None, alias="cpuCount") cpu_model: Optional[str] = Field(None, alias="cpuModel") total_ram: Optional[str] = Field(None, alias="totalRam") @@ -249,8 +270,16 @@ class DeleteNodeResponseDto(BaseModel): class RestartAllNodesRequestBodyDto(BaseModel): + model_config = ConfigDict(populate_by_name=True) + force_restart: bool = Field(default=False, alias="forceRestart") - + + +class RestartNodeRequestBodyDto(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + force_restart: bool = Field(default=False, alias="forceRestart") + class ResetNodeTrafficRequestDto(BaseModel): uuid: Union[str, UUID] = Field(alias="uuid") diff --git a/remnawave/models/system.py b/remnawave/models/system.py index 42ff921..21d78fe 100644 --- a/remnawave/models/system.py +++ b/remnawave/models/system.py @@ -198,18 +198,6 @@ class GetX25519KeyPairResponseDto(BaseModel): GenerateX25519ResponseDto = GetX25519KeyPairResponseDto -class EncryptHappCryptoLinkRequestDto(BaseModel): - link_to_encrypt: str = Field(serialization_alias="linkToEncrypt") - - -class EncryptHappCryptoLinkData(BaseModel): - encrypted_link: str = Field(alias="encryptedLink") - - -class EncryptHappCryptoLinkResponseDto(EncryptHappCryptoLinkData): - pass - - class DebugSrrMatcherRequestDto(BaseModel): response_rules: ResponseRules = Field(serialization_alias="responseRules") diff --git a/remnawave/models/users.py b/remnawave/models/users.py index 2499538..18bff5b 100644 --- a/remnawave/models/users.py +++ b/remnawave/models/users.py @@ -62,7 +62,7 @@ class CreateUserRequestDto(BaseModel): ss_password: Optional[Annotated[str, StringConstraints(min_length=8, max_length=32)]] = Field( None, serialization_alias="ssPassword" ) - traffic_limit_bytes: Optional[int] = Field( + traffic_limit_bytes: Optional[float] = Field( None, serialization_alias="trafficLimitBytes", ge=0 ) created_at: Optional[datetime] = Field(None, serialization_alias="createdAt") @@ -97,7 +97,7 @@ class UpdateUserRequestDto(BaseModel): 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_bytes: Optional[float] = Field(None, serialization_alias="trafficLimitBytes", ge=0) traffic_limit_strategy: Optional[TrafficLimitStrategy] = Field( None, serialization_alias="trafficLimitStrategy" ) @@ -123,7 +123,7 @@ class UserResponseDto(BaseModel): short_uuid: str = Field(alias="shortUuid") username: str status: UserStatus = Field(default=UserStatus.ACTIVE) - traffic_limit_bytes: int = Field(0, alias="trafficLimitBytes") + traffic_limit_bytes: float = Field(0, alias="trafficLimitBytes") traffic_limit_strategy: TrafficLimitStrategy = Field( TrafficLimitStrategy.NO_RESET, alias="trafficLimitStrategy" ) @@ -300,6 +300,22 @@ class GetAllUsersResponseDto(UsersResponseDto): pass +class UsersStreamData(BaseModel): + """Cursor-based (keyset) users stream page""" + users: list[UserResponseDto] + next_cursor: Optional[str] = Field( + None, + alias="nextCursor", + description="Cursor to fetch the next page, or null if there are no more results", + ) + has_more: bool = Field(alias="hasMore", description="Whether there are more results to fetch") + + +class GetUsersStreamResponseDto(UsersStreamData): + """Response for get all users using cursor-based (keyset) pagination""" + pass + + class TagsResponseDto(BaseModel): """Tags collection response""" tags: list[str] diff --git a/remnawave/models/users_bulk_actions.py b/remnawave/models/users_bulk_actions.py index e46587e..424ab8d 100644 --- a/remnawave/models/users_bulk_actions.py +++ b/remnawave/models/users_bulk_actions.py @@ -38,9 +38,9 @@ class BulkResetTrafficUsersRequestDto(BaseModel): class UpdateUserFields(BaseModel): """Fields to update for users""" status: Optional[UserStatus] = None - traffic_limit_bytes: Optional[int] = Field( - None, - serialization_alias="trafficLimitBytes", + traffic_limit_bytes: Optional[float] = Field( + None, + serialization_alias="trafficLimitBytes", ge=0, description="Traffic limit in bytes. 0 - unlimited" ) @@ -98,7 +98,7 @@ class BulkExtendExpirationDateRequestDto(BaseModel): class BulkAllUpdateUsersRequestDto(BaseModel): """Request to update all users""" status: Optional[UserStatus] = Field(default=UserStatus.ACTIVE) - traffic_limit_bytes: Optional[int] = Field( + traffic_limit_bytes: Optional[float] = Field( None, serialization_alias="trafficLimitBytes", ge=0, diff --git a/tests/test_controllers_completeness.py b/tests/test_controllers_completeness.py index 7060224..bd36561 100644 --- a/tests/test_controllers_completeness.py +++ b/tests/test_controllers_completeness.py @@ -5,6 +5,9 @@ import inspect from remnawave.controllers.users import UsersController from remnawave.controllers.system import SystemController from remnawave.controllers.ip_control import IpControlController +from remnawave.controllers.api_tokens_management import APITokensManagementController +from remnawave.controllers.hosts_bulk_actions import HostsBulkActionsController +from remnawave.controllers.bandwidthstats import BandWidthStatsController class TestUsersControllerEndpoints: @@ -66,6 +69,10 @@ class TestUsersControllerEndpoints: def test_has_get_user_subscription_request_history(self): assert hasattr(UsersController, "get_user_subscription_request_history") + def test_has_get_users_stream(self): + assert hasattr(UsersController, "get_users_stream") + assert callable(getattr(UsersController, "get_users_stream")) + class TestSystemControllerEndpoints: def test_has_get_recap(self): @@ -93,12 +100,39 @@ class TestSystemControllerEndpoints: def test_has_get_x25519_key_pair(self): assert hasattr(SystemController, "get_x25519_key_pair") - def test_has_encrypt_happ_crypto_link(self): - assert hasattr(SystemController, "encrypt_happ_crypto_link") - def test_has_debug_srr_matcher(self): assert hasattr(SystemController, "debug_srr_matcher") + def test_no_encrypt_happ_crypto_link(self): + # Removed in Remnawave API v2.8.0 (use client-side happ link generation instead) + assert not hasattr(SystemController, "encrypt_happ_crypto_link") + + +class TestApiTokensControllerEndpoints: + def test_has_get_scopes(self): + assert hasattr(APITokensManagementController, "get_scopes") + assert callable(getattr(APITokensManagementController, "get_scopes")) + + +class TestHostsBulkActionsControllerEndpoints: + def test_has_update_hosts(self): + assert hasattr(HostsBulkActionsController, "update_hosts") + assert callable(getattr(HostsBulkActionsController, "update_hosts")) + + def test_no_set_inbound_to_hosts(self): + # Removed in Remnawave API v2.8.0 (replaced by update_hosts) + assert not hasattr(HostsBulkActionsController, "set_inbound_to_hosts") + + def test_no_set_port_to_hosts(self): + # Removed in Remnawave API v2.8.0 (replaced by update_hosts) + assert not hasattr(HostsBulkActionsController, "set_port_to_hosts") + + +class TestBandwidthStatsControllerEndpoints: + def test_has_get_stats_nodes_users_usage(self): + assert hasattr(BandWidthStatsController, "get_stats_nodes_users_usage") + assert callable(getattr(BandWidthStatsController, "get_stats_nodes_users_usage")) + class TestIpControlControllerEndpoints: def test_has_fetch_user_ips(self): diff --git a/tests/test_models_validation.py b/tests/test_models_validation.py index e3f71da..433578f 100644 --- a/tests/test_models_validation.py +++ b/tests/test_models_validation.py @@ -3,6 +3,8 @@ import pytest from datetime import datetime, timezone from uuid import uuid4 +from pydantic import ValidationError + from remnawave.models import ( # Users ResolveUserRequestBodyDto, @@ -180,12 +182,13 @@ class TestCreateInfraBillingHistoryRecordRequestDto: class TestCreateInfraBillingNodeRequestDto: - def test_next_billing_at_optional(self): - dto = CreateInfraBillingNodeRequestDto( - node_uuid=uuid4(), - provider_uuid=uuid4(), - ) - assert dto.next_billing_at is None + def test_next_billing_at_required(self): + """next_billing_at became required in Remnawave API v2.8.0.""" + with pytest.raises(ValidationError): + CreateInfraBillingNodeRequestDto( + node_uuid=uuid4(), + provider_uuid=uuid4(), + ) def test_next_billing_at_provided(self): now = datetime.now(tz=timezone.utc) @@ -196,6 +199,17 @@ class TestCreateInfraBillingNodeRequestDto: ) assert dto.next_billing_at == now + def test_name_supported_and_node_uuid_optional(self): + """name was added and node_uuid became nullable in Remnawave API v2.8.0.""" + now = datetime.now(tz=timezone.utc) + dto = CreateInfraBillingNodeRequestDto( + provider_uuid=uuid4(), + name="My server", + next_billing_at=now, + ) + assert dto.name == "My server" + assert dto.node_uuid is None + class TestResponseRulesSettings: def test_settings_field_exists(self):