From 936c006b9b31a5ed6c08a46e6a89dc72953fdc6f Mon Sep 17 00:00:00 2001 From: Artem Date: Sat, 11 Oct 2025 21:40:52 +0200 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8=20SDK=20=D0=B4?= =?UTF-8?q?=D0=BE=202.1.18=20=D0=B8=202.1.17;=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B5=20DTO?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B8=20=D0=B2=D0=B5=D0=B1=D1=85=D1=83=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B8=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D1=81=D1=83=D1=89=D0=B5=D1=81=D1=82=D0=B2=D1=83=D1=8E?= =?UTF-8?q?=D1=89=D0=B8=D0=B5=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- pyproject.toml | 4 +- remnawave/controllers/hosts_bulk_actions.py | 4 +- remnawave/controllers/webhooks.py | 4 +- remnawave/models/__init__.py | 6 +- remnawave/models/hosts.py | 143 +++++------ remnawave/models/hosts_bulk_actions.py | 8 +- remnawave/models/subscription.py | 252 +++++++++++--------- remnawave/models/webhook.py | 112 ++++++--- tests/test_hosts.py | 2 +- 10 files changed, 304 insertions(+), 234 deletions(-) diff --git a/README.md b/README.md index da23460..36be63e 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.17 | >=2.1.16 | +| 2.1.18 | >=2.1.18 | +| 2.1.17 | >=2.1.16, <=2.1.17 | | 2.1.16 | >=2.1.16 | | 2.1.13 | >=2.1.13, <=2.1.15 | | 2.1.9 | >=2.1.9, <=2.1.12 | diff --git a/pyproject.toml b/pyproject.toml index 08bf23c..3df7550 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "remnawave" -version = "2.1.17" -description = "A Python SDK for interacting with the Remnawave API v2.1.17." +version = "2.1.18" +description = "A Python SDK for interacting with the Remnawave API v2.1.18." authors = [ {name = "Artem",email = "dev@forestsnet.com"} ] diff --git a/remnawave/controllers/hosts_bulk_actions.py b/remnawave/controllers/hosts_bulk_actions.py index 4a9d62d..6f4d759 100644 --- a/remnawave/controllers/hosts_bulk_actions.py +++ b/remnawave/controllers/hosts_bulk_actions.py @@ -10,6 +10,7 @@ from remnawave.models import ( SetInboundToManyHostsRequestDto, SetInboundToManyHostsResponseDto, SetPortToManyHostsResponseDto, + SetPortToManyHostsRequestDto ) from remnawave.rapid import AttributeBody, BaseController, post @@ -53,8 +54,7 @@ class HostsBulkActionsController(BaseController): @post("/hosts/bulk/set-port", response_class=SetPortToManyHostsResponseDto) async def set_port_to_hosts( self, - uuids: Annotated[List[UUID], AttributeBody()], - port: Annotated[float, AttributeBody()], + body: Annotated[SetPortToManyHostsRequestDto, PydanticBody()], ) -> SetPortToManyHostsResponseDto: """Set port to many hosts""" ... diff --git a/remnawave/controllers/webhooks.py b/remnawave/controllers/webhooks.py index 6d501a8..ecc9951 100644 --- a/remnawave/controllers/webhooks.py +++ b/remnawave/controllers/webhooks.py @@ -6,7 +6,7 @@ from typing import Union, Optional from remnawave.models.webhook import ( WebhookPayloadDto, UserDto, - NodesDto, + NodeDto, HwidUserDeviceDto, LoginAttemptDto, UserHwidDeviceEventDto, @@ -148,7 +148,7 @@ class WebhookUtility: return event.startswith("errors.") @staticmethod - def get_typed_data(payload: WebhookPayloadDto) -> Union[UserDto, NodesDto, HwidUserDeviceDto, LoginAttemptDto, UserHwidDeviceEventDto, dict]: + def get_typed_data(payload: WebhookPayloadDto) -> Union[UserDto, NodeDto, HwidUserDeviceDto, LoginAttemptDto, UserHwidDeviceEventDto, dict]: """ Get typed data from webhook payload based on event type. diff --git a/remnawave/models/__init__.py b/remnawave/models/__init__.py index e50e7e4..5465b27 100644 --- a/remnawave/models/__init__.py +++ b/remnawave/models/__init__.py @@ -63,6 +63,7 @@ from .hosts_bulk_actions import ( SetInboundToManyHostsRequestDto, SetInboundToManyHostsResponseDto, SetPortToManyHostsResponseDto, + SetPortToManyHostsRequestDto ) from .hwid import ( CreateHWIDUser, # Legacy alias @@ -252,7 +253,7 @@ from .webhook import ( InternalSquadDto, BaseUserDto, UserDto, - NodesDto, + NodeDto, ConfigProfileInboundDto, InfraProviderDto, LoginAttemptDto, @@ -404,6 +405,7 @@ __all__ = [ "SetInboundToManyHostsRequestDto", "SetInboundToManyHostsResponseDto", "SetPortToManyHostsResponseDto", + "SetPortToManyHostsRequestDto", # Users models "ActiveInternalSquadDto", "CreateUserRequestDto", @@ -524,7 +526,7 @@ __all__ = [ # NODE ENTITIES "ConfigProfileInboundDto", "InfraProviderDto", - "NodesDto", + "NodeDto", "NodeEventDto", # ERROR EVENTS diff --git a/remnawave/models/hosts.py b/remnawave/models/hosts.py index 3382d69..69a44db 100644 --- a/remnawave/models/hosts.py +++ b/remnawave/models/hosts.py @@ -40,9 +40,9 @@ class UpdateHostRequestDto(BaseModel): None, serialization_alias="securityLayer" ) server_description: Optional[str] = Field( - None, alias="serverDescription", max_length=30 + None, serialization_alias="serverDescription", max_length=30 ) - muxParams: Optional[str] = Field( + mux_params: Optional[str] = Field( None, serialization_alias="muxParams", ) @@ -67,19 +67,24 @@ class UpdateHostRequestDto(BaseModel): ge=0, le=65535 ) - shuffle_host: bool = Field( - False, + shuffle_host: Optional[bool] = Field( + None, serialization_alias="shuffleHost", ) - mihomo_x25519: bool = Field( - False, + mihomo_x25519: Optional[bool] = Field( + None, serialization_alias="mihomoX25519", ) + x_http_extra_params: Optional[str] = Field( + None, + serialization_alias="xHttpExtraParams", + ) + nodes: Optional[List[str]] = None class HostInboundData(BaseModel): - config_profile_uuid: UUID = Field(alias="configProfileUuid") - config_profile_inbound_uuid: UUID = Field(alias="configProfileInboundUuid") + config_profile_uuid: Optional[UUID] = Field(alias="configProfileUuid") + config_profile_inbound_uuid: Optional[UUID] = Field(alias="configProfileInboundUuid") class HostResponseDto(BaseModel): @@ -93,57 +98,54 @@ class HostResponseDto(BaseModel): host: Optional[str] = None alpn: Optional[ALPN] = None fingerprint: Optional[Fingerprint] = None - is_disabled: Optional[bool] = Field( - None, - alias="isDisabled", - ) - security_layer: Optional[SecurityLayer] = Field( - None, - alias="securityLayer", - ) x_http_extra_params: Optional[str] = Field( None, alias="xHttpExtraParams", ) - server_description: Optional[str] = Field( - None, alias="serverDescription", max_length=30 - ) - inbound: HostInboundData - muxParams: Optional[str] = Field( + mux_params: Optional[str] = Field( None, - serialization_alias="muxParams", + alias="muxParams", ) sockopt_params: Optional[str] = Field( None, - serialization_alias="sockoptParams", + alias="sockoptParams", ) - tag: Optional[Annotated[str, StringConstraints(max_length=32)]] = Field( - None, serialization_alias="tag" - ) - is_hidden: Optional[bool] = Field( - None, - serialization_alias="isHidden", - ) - override_sni_from_address: Optional[bool] = Field( - None, - serialization_alias="overrideSniFromAddress", + inbound: HostInboundData + server_description: Optional[str] = Field( + None, alias="serverDescription" ) + tag: Optional[str] = None vless_route_id: Optional[int] = Field( None, - serialization_alias="vlessRouteId", + alias="vlessRouteId", ) - shuffle_host: bool = Field( - False, - serialization_alias="shuffleHost", + shuffle_host: bool = Field(alias="shuffleHost") + mihomo_x25519: bool = Field(alias="mihomoX25519") + nodes: List[str] + is_disabled: bool = Field( + default=False, + alias="isDisabled", ) - mihomo_x25519: bool = Field( - False, - serialization_alias="mihomoX25519", + security_layer: SecurityLayer = Field( + default=SecurityLayer.DEFAULT, + alias="securityLayer", + ) + is_hidden: bool = Field( + default=False, + alias="isHidden", + ) + override_sni_from_address: bool = Field( + default=False, + alias="overrideSniFromAddress", + ) + allow_insecure: bool = Field( + default=False, + alias="allowInsecure", ) # Legacy compatibility property @property - def inbound_uuid(self) -> UUID: + def inbound_uuid(self) -> Optional[UUID]: return self.inbound.config_profile_inbound_uuid @@ -186,8 +188,8 @@ class DeleteHostResponseDto(BaseModel): class CreateHostInboundData(BaseModel): - config_profile_uuid: UUID = Field(serialization_alias="configProfileUuid") - config_profile_inbound_uuid: UUID = Field( + config_profile_uuid: Optional[UUID] = Field(serialization_alias="configProfileUuid") + config_profile_inbound_uuid: Optional[UUID] = Field( serialization_alias="configProfileInboundUuid" ) @@ -202,19 +204,11 @@ class CreateHostRequestDto(BaseModel): host: Optional[str] = None alpn: Optional[ALPN] = None fingerprint: Optional[Fingerprint] = None - allow_insecure: bool = Field( - False, - serialization_alias="allowInsecure", + x_http_extra_params: Optional[str] = Field( + None, + serialization_alias="xHttpExtraParams", ) - is_disabled: bool = Field( - False, - serialization_alias="isDisabled", - ) - security_layer: Optional[SecurityLayer] = Field( - SecurityLayer.DEFAULT, - serialization_alias="securityLayer", - ) - muxParams: Optional[str] = Field( + mux_params: Optional[str] = Field( None, serialization_alias="muxParams", ) @@ -222,19 +216,11 @@ class CreateHostRequestDto(BaseModel): None, serialization_alias="sockoptParams", ) - tag: Optional[Annotated[str, StringConstraints(max_length=32, pattern="^[A-Z0-9_:]+$")]] = Field( - None, serialization_alias="tag" - ) - is_hidden: bool = Field( - False, - serialization_alias="isHidden", - ) - override_sni_from_address: bool = Field( - False, - serialization_alias="overrideSniFromAddress", - ) server_description: Optional[str] = Field( - None, alias="serverDescription", max_length=30 + None, serialization_alias="serverDescription", max_length=30 + ) + tag: Optional[Annotated[str, StringConstraints(max_length=32)]] = Field( + None, serialization_alias="tag" ) vless_route_id: Optional[int] = Field( None, @@ -250,10 +236,31 @@ class CreateHostRequestDto(BaseModel): False, serialization_alias="mihomoX25519", ) + nodes: List[str] = Field(default_factory=list) + allow_insecure: bool = Field( + False, + serialization_alias="allowInsecure", + ) + is_disabled: bool = Field( + False, + serialization_alias="isDisabled", + ) + security_layer: SecurityLayer = Field( + SecurityLayer.DEFAULT, + serialization_alias="securityLayer", + ) + is_hidden: bool = Field( + False, + serialization_alias="isHidden", + ) + override_sni_from_address: bool = Field( + False, + serialization_alias="overrideSniFromAddress", + ) # Legacy compatibility property @property - def inbound_uuid(self) -> UUID: + def inbound_uuid(self) -> Optional[UUID]: return self.inbound.config_profile_inbound_uuid # Constructor compatibility - support old-style inbound_uuid diff --git a/remnawave/models/hosts_bulk_actions.py b/remnawave/models/hosts_bulk_actions.py index e16b582..de043c8 100644 --- a/remnawave/models/hosts_bulk_actions.py +++ b/remnawave/models/hosts_bulk_actions.py @@ -8,8 +8,14 @@ from remnawave.models import HostResponseDto class SetInboundToManyHostsRequestDto(BaseModel): uuids: List[UUID] - inbound_uuid: UUID = Field(serialization_alias="inboundUuid") + config_profile_uuid: UUID = Field(serialization_alias="configProfileUuid") + config_profile_inbound_uuid: UUID = Field( + serialization_alias="configProfileInboundUuid" + ) +class SetPortToManyHostsRequestDto(BaseModel): + uuids: List[UUID] + port: int = Field(ge=1, le=65535) class BulkDeleteHostsResponseDto(List[HostResponseDto]): pass diff --git a/remnawave/models/subscription.py b/remnawave/models/subscription.py index 9012360..00f1fcd 100644 --- a/remnawave/models/subscription.py +++ b/remnawave/models/subscription.py @@ -11,6 +11,136 @@ class HappCrypto(BaseModel): crypto_link: str = Field(alias="cryptoLink") +class UserLastConnectedNodeDto(BaseModel): + connected_at: datetime = Field(alias="connectedAt") + node_name: str = Field(alias="nodeName") + country_code: str = Field(alias="countryCode") + + +class ActiveInternalSquadDto(BaseModel): + uuid: UUID + name: str + + +class UserResponseDto(BaseModel): + uuid: UUID + short_uuid: str = Field(alias="shortUuid") + username: str + status: UserStatus = Field(default=UserStatus.ACTIVE) + used_traffic_bytes: float = Field(alias="usedTrafficBytes") + lifetime_used_traffic_bytes: float = Field(alias="lifetimeUsedTrafficBytes") + traffic_limit_bytes: int = Field(default=0, alias="trafficLimitBytes") + traffic_limit_strategy: TrafficLimitStrategy = Field( + default=TrafficLimitStrategy.NO_RESET, + alias="trafficLimitStrategy" + ) + sub_last_user_agent: Optional[str] = Field(None, alias="subLastUserAgent") + sub_last_opened_at: Optional[datetime] = Field(None, alias="subLastOpenedAt") + expire_at: datetime = Field(alias="expireAt") + online_at: Optional[datetime] = Field(None, alias="onlineAt") + sub_revoked_at: Optional[datetime] = Field(None, alias="subRevokedAt") + last_traffic_reset_at: Optional[datetime] = Field(None, alias="lastTrafficResetAt") + trojan_password: str = Field(alias="trojanPassword") + vless_uuid: UUID = Field(alias="vlessUuid") + ss_password: str = Field(alias="ssPassword") + description: Optional[str] = None + tag: Optional[str] = None + telegram_id: Optional[int] = Field(None, alias="telegramId") + email: Optional[str] = None + hwid_device_limit: Optional[int] = Field(None, alias="hwidDeviceLimit") + first_connected_at: Optional[datetime] = Field(None, alias="firstConnectedAt") + last_triggered_threshold: int = Field(default=0, alias="lastTriggeredThreshold") + created_at: datetime = Field(alias="createdAt") + updated_at: datetime = Field(alias="updatedAt") + active_internal_squads: List[ActiveInternalSquadDto] = Field(alias="activeInternalSquads") + subscription_url: str = Field(alias="subscriptionUrl") + last_connected_node: Optional[UserLastConnectedNodeDto] = Field(None, alias="lastConnectedNode") + happ: HappCrypto = Field(alias="happ") + + +class ConvertedUserInfo(BaseModel): + days_left: int = Field(alias="daysLeft") + traffic_limit: str = Field(alias="trafficLimit") + traffic_used: str = Field(alias="trafficUsed") + lifetime_traffic_used: str = Field(alias="lifetimeTrafficUsed") + is_hwid_limited: bool = Field(alias="isHwidLimited") + + +class Passwords(BaseModel): + ss_password: str = Field(alias="ssPassword") + trojan_password: str = Field(alias="trojanPassword") + vless_password: str = Field(alias="vlessPassword") + + +class RawHostAdditionalParams(BaseModel): + mode: Optional[str] = None + heartbeat_period: Optional[float] = Field(None, alias="heartbeatPeriod") + + +class RawHostProtocolOptions(BaseModel): + class SSOptions(BaseModel): + method: Optional[str] = None + + ss: Optional[SSOptions] = None + + +class RawHostDbData(BaseModel): + raw_inbound: Optional[Dict[str, Any]] = Field(alias="rawInbound") + inbound_tag: str = Field(alias="inboundTag") + uuid: str + config_profile_uuid: Optional[str] = Field(alias="configProfileUuid") + config_profile_inbound_uuid: Optional[str] = Field(alias="configProfileInboundUuid") + is_disabled: bool = Field(alias="isDisabled") + view_position: int = Field(alias="viewPosition") + remark: str + is_hidden: bool = Field(alias="isHidden") + tag: Optional[str] = None + vless_route_id: Optional[int] = Field(None, alias="vlessRouteId") + + +class RawHost(BaseModel): + password: Passwords + address: Optional[str] = None + alpn: Optional[str] = None + fingerprint: Optional[str] = None + host: Optional[str] = None + network: Optional[str] = None + path: Optional[str] = None + public_key: Optional[str] = Field(None, alias="publicKey") + port: Optional[float] = None # изменен тип на float + protocol: Optional[str] = None + remark: Optional[str] = None + short_id: Optional[str] = Field(None, alias="shortId") + sni: Optional[str] = None + spider_x: Optional[str] = Field(None, alias="spiderX") + tls: Optional[str] = None + header_type: Optional[str] = Field(None, alias="headerType") + additional_params: Optional[RawHostAdditionalParams] = Field(None, alias="additionalParams") + x_http_extra_params: Optional[Dict[str, Any]] = Field(None, alias="xHttpExtraParams") + mux_params: Optional[Dict[str, Any]] = Field(None, alias="muxParams") + sockopt_params: Optional[Dict[str, Any]] = Field(None, alias="sockoptParams") + server_description: Optional[str] = Field(None, alias="serverDescription") + flow: Optional[str] = None + allow_insecure: Optional[bool] = Field(None, alias="allowInsecure") + shuffle_host: Optional[bool] = Field(None, alias="shuffleHost") + mihomo_x25519: Optional[bool] = Field(None, alias="mihomoX25519") + mldsa65_verify: Optional[str] = Field(None, alias="mldsa65Verify") + encryption: Optional[str] = None + protocol_options: Optional[RawHostProtocolOptions] = Field(None, alias="protocolOptions") + db_data: RawHostDbData = Field(alias="dbData") + + +class RawSubscriptionResponse(BaseModel): + user: UserResponseDto + converted_user_info: ConvertedUserInfo = Field(alias="convertedUserInfo") + headers: Dict[str, str] + raw_hosts: List[RawHost] = Field(alias="rawHosts") + + +class GetRawSubscriptionByShortUuidResponseDto(RawSubscriptionResponse): + pass + +# Legacy alias for backward compatibility class UserSubscription(BaseModel): short_uuid: str = Field(alias="shortUuid") username: str @@ -26,6 +156,7 @@ class UserSubscription(BaseModel): user_status: UserStatus = Field(alias="userStatus") is_active: bool = Field(alias="isActive") + class SubscriptionInfoData(BaseModel): is_found: bool = Field(alias="isFound") user: UserSubscription @@ -43,122 +174,6 @@ class GetSubscriptionInfoResponseDto(BaseModel): subscription_url: str = Field(alias="subscriptionUrl") happ: HappCrypto -class UserLastConnectedNodeDto(BaseModel): - connected_at: datetime = Field(alias="connectedAt") - node_name: str = Field(alias="nodeName") - country_code: Optional[str] = Field(None, alias="countryCode") # новое поле - -class ActiveInternalSquadDto(BaseModel): - uuid: UUID - name: str - -class HappCrypto(BaseModel): - crypto_link: str = Field(alias="cryptoLink") - -class UserResponseDto(BaseModel): - uuid: UUID - short_uuid: str = Field(alias="shortUuid") - username: str - status: str - used_traffic_bytes: float = Field(alias="usedTrafficBytes") - lifetime_used_traffic_bytes: float = Field(alias="lifetimeUsedTrafficBytes") - traffic_limit_bytes: int = Field(alias="trafficLimitBytes") - traffic_limit_strategy: str = Field(alias="trafficLimitStrategy") - sub_last_user_agent: Optional[str] = Field(None, alias="subLastUserAgent") - sub_last_opened_at: Optional[datetime] = Field(None, alias="subLastOpenedAt") - expire_at: datetime = Field(alias="expireAt") - online_at: Optional[datetime] = Field(None, alias="onlineAt") - sub_revoked_at: Optional[datetime] = Field(None, alias="subRevokedAt") - last_traffic_reset_at: Optional[datetime] = Field(None, alias="lastTrafficResetAt") - trojan_password: str = Field(alias="trojanPassword") - vless_uuid: UUID = Field(alias="vlessUuid") - ss_password: str = Field(alias="ssPassword") - description: Optional[str] = None - tag: Optional[str] = None - telegram_id: Optional[int] = Field(None, alias="telegramId") - email: Optional[str] = None - hwidDeviceLimit: Optional[int] = Field(None, alias="hwidDeviceLimit") - first_connected_at: Optional[datetime] = Field(None, alias="firstConnectedAt") - last_triggered_threshold: int = Field(alias="lastTriggeredThreshold") - created_at: datetime = Field(alias="createdAt") - updated_at: datetime = Field(alias="updatedAt") - active_internal_squads: List[ActiveInternalSquadDto] = Field(alias="activeInternalSquads") - subscription_url: str = Field(alias="subscriptionUrl") - last_connected_node: Optional[UserLastConnectedNodeDto] = Field(None, alias="lastConnectedNode") - happ: Optional[HappCrypto] = Field(None, alias="happ") - -class ConvertedUserInfo(BaseModel): - days_left: int = Field(alias="daysLeft") - traffic_limit: str = Field(alias="trafficLimit") - traffic_used: str = Field(alias="trafficUsed") - lifetime_traffic_used: str = Field(alias="lifetimeTrafficUsed") - is_hwid_limited: bool = Field(alias="isHwidLimited") - -class Passwords(BaseModel): - ss_password: Optional[str] = Field(None, alias="ssPassword") - trojan_password: Optional[str] = Field(None, alias="trojanPassword") - vless_password: Optional[str] = Field(None, alias="vlessPassword") - - - -class RawHostAdditionalParams(BaseModel): - mode: Optional[str] = None - heartbeat_period: Optional[int] = Field(None, alias="heartbeatPeriod") - - -class RawHostProtocolOptions(BaseModel): - class SSOptions(BaseModel): - method: Optional[str] = None - - ss: Optional[SSOptions] = None - - -class RawHostDbData(BaseModel): - raw_inbound: Optional[Dict[str, Any]] = Field(alias="rawInbound") - inbound_tag: str = Field(alias="inboundTag") - uuid: str - config_profile_uuid: Optional[str] = Field(alias="configProfileUuid") - config_profile_inbound_uuid: Optional[str] = Field(alias="configProfileInboundUuid") - is_disabled: bool = Field(alias="isDisabled") - view_position: int = Field(alias="viewPosition") - remark: str - is_hidden: bool = Field(alias="isHidden") - tag: Optional[str] = None - -class RawHost(BaseModel): - address: Optional[str] = None - alpn: Optional[str] = None - fingerprint: Optional[str] = None - host: Optional[str] = None - network: Optional[str] = None - password: Optional[Passwords] = None - path: Optional[str] = None - public_key: Optional[str] = Field(None, alias="publicKey") - port: Optional[int] = None - protocol: Optional[str] = None - remark: Optional[str] = None - short_id: Optional[str] = Field(None, alias="shortId") - sni: Optional[str] = None - spider_x: Optional[str] = Field(None, alias="spiderX") - tls: Optional[str] = None - header_type: Optional[str] = Field(None, alias="headerType") - additional_params: Optional[RawHostAdditionalParams] = Field(None, alias="additionalParams") - x_http_extra_params: Optional[Dict[str, Any]] = Field(None, alias="xHttpExtraParams") - mux_params: Optional[Dict[str, Any]] = Field(None, alias="muxParams") - sockopt_params: Optional[Dict[str, Any]] = Field(None, alias="sockoptParams") - server_description: Optional[str] = Field(None, alias="serverDescription") - flow: Optional[str] = None - protocol_options: Optional[RawHostProtocolOptions] = Field(None, alias="protocolOptions") - db_data: RawHostDbData = Field(alias="dbData") - -class RawSubscriptionResponse(BaseModel): - user: UserResponseDto - converted_user_info: ConvertedUserInfo = Field(alias="convertedUserInfo") - headers: Dict[str, str] - raw_hosts: List[RawHost] = Field(alias="rawHosts") - -class GetRawSubscriptionByShortUuidResponseDto(RawSubscriptionResponse): - pass class SubscriptionWithoutHapp(BaseModel): is_found: bool = Field(alias="isFound") @@ -180,11 +195,14 @@ class GetSubscriptionByUsernameResponseDto(BaseModel): ss_conf_links: Dict[str, str] = Field(alias="ssConfLinks") subscription_url: str = Field(alias="subscriptionUrl") + class GetSubscriptionByShortUUIDResponseDto(GetSubscriptionByUsernameResponseDto): pass + class GetSubscriptionByUUIDResponseDto(GetSubscriptionByUsernameResponseDto): pass + # Legacy alias for backward compatibility -SubscriptionInfoResponseDto = GetSubscriptionInfoResponseDto +SubscriptionInfoResponseDto = GetSubscriptionInfoResponseDto \ No newline at end of file diff --git a/remnawave/models/webhook.py b/remnawave/models/webhook.py index 2dd2a51..4f34b3a 100644 --- a/remnawave/models/webhook.py +++ b/remnawave/models/webhook.py @@ -19,7 +19,6 @@ class LastConnectedNodeDto(BaseModel): class InternalSquadDto(BaseModel): uuid: UUID name: str - description: Optional[str] = None model_config = {"alias_generator": to_camel, "populate_by_name": True} @@ -29,11 +28,10 @@ class BaseUserDto(BaseModel): short_uuid: str username: str status: TUsersStatus + used_traffic_bytes: int + lifetime_used_traffic_bytes: int - used_traffic_bytes: str - lifetime_used_traffic_bytes: str - - traffic_limit_bytes: str + traffic_limit_bytes: int traffic_limit_strategy: TResetPeriods sub_last_user_agent: Optional[str] = None sub_last_opened_at: Optional[datetime] = None @@ -48,7 +46,7 @@ class BaseUserDto(BaseModel): description: Optional[str] = None tag: Optional[str] = None - telegram_id: Optional[str] = None + telegram_id: Optional[int] = None email: Optional[str] = None hwid_device_limit: Optional[int] = None @@ -57,7 +55,7 @@ class BaseUserDto(BaseModel): last_triggered_threshold: int online_at: Optional[datetime] = None - last_connected_node_uuid: Optional[str] = None + last_connected_node_uuid: Optional[UUID] = None created_at: datetime updated_at: datetime @@ -73,9 +71,8 @@ class UserDto(BaseUserDto): class UserEventDto(BaseModel): - user: UserDto event_name: TUserEvents - skip_telegram_notification: bool = False + user: UserDto model_config = {"alias_generator": to_camel, "populate_by_name": True} @@ -106,7 +103,6 @@ class UserHwidDeviceEventDto(BaseModel): def build(cls, user: UserDto, hwid_device: HwidUserDeviceDto, event: TUserHwidDevicesEvents): return cls(data={"user": user, "hwidUserDevice": hwid_device}, event_name=event) - # ---------------- SERVICE EVENTS ---------------- # class LoginAttemptDto(BaseModel): @@ -115,7 +111,7 @@ class LoginAttemptDto(BaseModel): user_agent: str description: Optional[str] = None password: Optional[str] = None - + model_config = {"alias_generator": to_camel, "populate_by_name": True} @@ -130,11 +126,30 @@ class ServiceEventDto(BaseModel): class ConfigProfileInboundDto(BaseModel): uuid: UUID - name: str - config: dict + profile_uuid: UUID - created_at: datetime - updated_at: datetime + tag: str + type: str + network: Optional[str] + security: Optional[str] + port: Optional[int] + + raw_inbound: Optional[dict] + + model_config = {"alias_generator": to_camel, "populate_by_name": True} + + +class InfraBillingHistoryDto(BaseModel): + total_amount: int + total_bills: int + + model_config = {"alias_generator": to_camel, "populate_by_name": True} + + +class InfraBillingNodeDto(BaseModel): + node_uuid: UUID + name: str + country_code: str model_config = {"alias_generator": to_camel, "populate_by_name": True} @@ -148,13 +163,13 @@ class InfraProviderDto(BaseModel): created_at: datetime updated_at: datetime - billing_history: Optional[dict] = None - billing_nodes: Optional[List[dict]] = None + billing_history: Optional[InfraBillingHistoryDto] = None + billing_nodes: Optional[List[InfraBillingNodeDto]] = None model_config = {"alias_generator": to_camel, "populate_by_name": True} -class NodesDto(BaseModel): +class NodeDto(BaseModel): uuid: UUID name: str address: str @@ -164,7 +179,6 @@ class NodesDto(BaseModel): is_disabled: bool is_node_online: bool is_xray_running: bool - last_status_change: Optional[datetime] = None last_status_message: Optional[str] = None @@ -173,16 +187,16 @@ class NodesDto(BaseModel): xray_uptime: str users_online: Optional[int] = None - + is_traffic_tracking_active: bool traffic_reset_day: Optional[int] = None - traffic_limit_bytes: Optional[str] = None - traffic_used_bytes: Optional[str] = None + traffic_limit_bytes: Optional[int] = None + traffic_used_bytes: Optional[int] = None notify_percent: Optional[int] = None view_position: int country_code: str - consumption_multiplier: str + consumption_multiplier: int cpu_count: Optional[int] = None cpu_model: Optional[str] = None @@ -201,27 +215,43 @@ class NodesDto(BaseModel): class NodeEventDto(BaseModel): - node: NodesDto event_name: TNodeEvents + data: NodeDto + + model_config = {"alias_generator": to_camel, "populate_by_name": True} + +# ---------------- ERROR EVENTS ---------------- # + +# https://github.com/remnawave/backend/blob/main/src/queue/user-jobs/user-jobs.processor.ts#L224 +# Not implemented yet! + +class ErrorDto(BaseModel): + description: str model_config = {"alias_generator": to_camel, "populate_by_name": True} -# ---------------- ERROR EVENTS ---------------- # - class CustomErrorEventDto(BaseModel): event_name: TErrorsEvents - data: dict + data: ErrorDto model_config = {"alias_generator": to_camel, "populate_by_name": True} # ---------------- CRM EVENTS ---------------- # +class BillingNodeDto(BaseModel): + provider_name: str + node_name: str + next_billing_at: datetime + login_url: str + + model_config = {"alias_generator": to_camel, "populate_by_name": True} + + class CrmEventDto(BaseModel): event_name: TCRMEvents - data: dict - skip_telegram_notification: bool = False + data: BillingNodeDto model_config = {"alias_generator": to_camel, "populate_by_name": True} @@ -230,15 +260,16 @@ class CrmEventDto(BaseModel): class WebhookPayloadDto(BaseModel): event: str + timestamp: datetime data: Union[ UserDto, - NodesDto, + NodeDto, HwidUserDeviceDto, LoginAttemptDto, UserHwidDeviceEventDto, + BillingNodeDto, dict ] - timestamp: datetime model_config = {"alias_generator": to_camel, "populate_by_name": True} @@ -250,19 +281,24 @@ class WebhookPayloadDto(BaseModel): if event.startswith("user."): data = UserDto(**data_raw) elif event.startswith("user_hwid_devices."): - data = HwidUserDeviceDto(**data_raw) + user = UserDto(**data_raw["user"]) + hwid_device = HwidUserDeviceDto(**data_raw["hwidUserDevice"]) + data = UserHwidDeviceEventDto.build( + user=user, + hwid_device=hwid_device, + event=event, + ) elif event.startswith("node."): - data = NodesDto(**data_raw) + data = NodeDto(**data_raw) elif event.startswith("service."): - # может быть loginAttempt или другое - if "username" in data_raw and "ip" in data_raw: + if event.startswith("service.login_attempt"): data = LoginAttemptDto(**data_raw) - else: + else: # service.panel_started - содержит пустой json data = data_raw elif event.startswith("errors."): - data = data_raw + data = ErrorDto(**data_raw) elif event.startswith("crm."): - data = data_raw + data = BillingNodeDto(**data_raw) else: data = data_raw diff --git a/tests/test_hosts.py b/tests/test_hosts.py index 0be4a3c..24504dd 100644 --- a/tests/test_hosts.py +++ b/tests/test_hosts.py @@ -123,7 +123,7 @@ class TestHostsCRUD: # Создаем новый объект для обновления update_data = UpdateHostRequestDto( uuid=test_host.uuid, - serverDescription="Updated Host", + server_description="Updated Host", is_disabled=False # явно устанавливаем значение )