mirror of
https://github.com/remnawave/python-sdk.git
synced 2026-08-04 06:35:09 +00:00
Добавить классы и типы для обработки вебхуков; обновить модели и перечисления для поддержки новых событий
This commit is contained in:
parent
87b8d8c3d8
commit
1141e4ed93
4 changed files with 385 additions and 174 deletions
|
|
@ -1,9 +1,44 @@
|
|||
import hmac
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Union, Optional, Dict
|
||||
from remnawave.enums import UserEvent, NodeEvent, InfraBillingEvent, ServiceEvent
|
||||
from remnawave.models import WebhookHeadersWebhookDto, WebhookPayloadWebhookDto
|
||||
from typing import Union, Optional
|
||||
|
||||
from remnawave.models.webhook import (
|
||||
WebhookPayloadDto,
|
||||
UserDto,
|
||||
NodesDto,
|
||||
HwidUserDeviceDto,
|
||||
LoginAttemptDto,
|
||||
UserHwidDeviceEventDto,
|
||||
)
|
||||
|
||||
class WebhookHeadersDto:
|
||||
"""Helper class for webhook headers"""
|
||||
|
||||
def __init__(self, signature: str, timestamp: str):
|
||||
self.signature = signature
|
||||
self.timestamp = timestamp
|
||||
|
||||
@classmethod
|
||||
def from_headers(cls, headers: dict[str, str]) -> "WebhookHeadersDto":
|
||||
"""
|
||||
Create WebhookHeadersDto from headers dictionary.
|
||||
Handles case-insensitive header names.
|
||||
"""
|
||||
signature = None
|
||||
timestamp = None
|
||||
|
||||
for key, value in headers.items():
|
||||
lower_key = key.lower()
|
||||
if lower_key == "x-remnawave-signature":
|
||||
signature = value
|
||||
elif lower_key == "x-remnawave-timestamp":
|
||||
timestamp = value
|
||||
|
||||
if not signature or not timestamp:
|
||||
raise ValueError("Missing required webhook headers")
|
||||
|
||||
return cls(signature=signature, timestamp=timestamp)
|
||||
|
||||
|
||||
class WebhookUtility:
|
||||
|
|
@ -14,7 +49,12 @@ class WebhookUtility:
|
|||
webhook_secret: str
|
||||
) -> bool:
|
||||
"""
|
||||
Webhook authentication via HMAC SHA-256.
|
||||
Validates the webhook's authenticity using HMAC SHA-256.
|
||||
|
||||
:param body: The webhook request body (either a JSON string or a parsed dictionary).
|
||||
:param signature: The signature received from the server.
|
||||
:param webhook_secret: The secret key used to compute the HMAC.
|
||||
:return: True if the signature matches, otherwise False.
|
||||
"""
|
||||
if isinstance(body, str):
|
||||
original_body = body
|
||||
|
|
@ -22,8 +62,8 @@ class WebhookUtility:
|
|||
original_body = json.dumps(body, separators=(',', ':'))
|
||||
|
||||
computed_signature = hmac.new(
|
||||
webhook_secret.encode("utf-8"),
|
||||
original_body.encode("utf-8"),
|
||||
webhook_secret.encode('utf-8'),
|
||||
original_body.encode('utf-8'),
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
|
|
@ -32,26 +72,37 @@ class WebhookUtility:
|
|||
@staticmethod
|
||||
def validate_webhook_with_headers(
|
||||
body: Union[str, dict],
|
||||
headers: Union[Dict[str, str], WebhookHeadersWebhookDto],
|
||||
headers: Union[dict[str, str], WebhookHeadersDto],
|
||||
webhook_secret: str
|
||||
) -> bool:
|
||||
"""
|
||||
Checking webhook headers.
|
||||
Validates the webhook using headers object.
|
||||
|
||||
:param body: The webhook request body.
|
||||
:param headers: Dictionary with headers or WebhookHeadersDto object.
|
||||
:param webhook_secret: The secret key used to compute the HMAC.
|
||||
:return: True if the signature matches, otherwise False.
|
||||
"""
|
||||
if isinstance(headers, dict):
|
||||
headers = WebhookHeadersWebhookDto.from_headers(headers)
|
||||
|
||||
headers = WebhookHeadersDto.from_headers(headers)
|
||||
|
||||
return WebhookUtility.validate_webhook(body, headers.signature, webhook_secret)
|
||||
|
||||
@staticmethod
|
||||
def parse_webhook(
|
||||
body: Union[str, dict],
|
||||
headers: Union[Dict[str, str], WebhookHeadersWebhookDto],
|
||||
headers: Union[dict[str, str], WebhookHeadersDto],
|
||||
webhook_secret: str,
|
||||
validate: bool = True
|
||||
) -> Optional[WebhookPayloadWebhookDto]:
|
||||
) -> Optional[WebhookPayloadDto]:
|
||||
"""
|
||||
Parsing and (optional) validating the webhook payload.
|
||||
Parses and optionally validates the webhook payload.
|
||||
|
||||
:param body: The webhook request body.
|
||||
:param headers: Dictionary with headers or WebhookHeadersDto object.
|
||||
:param webhook_secret: The secret key used to compute the HMAC.
|
||||
:param validate: Whether to validate the webhook signature (default: True).
|
||||
:return: Parsed WebhookPayloadDto or None if validation fails.
|
||||
"""
|
||||
if validate and not WebhookUtility.validate_webhook_with_headers(body, headers, webhook_secret):
|
||||
return None
|
||||
|
|
@ -59,20 +110,71 @@ class WebhookUtility:
|
|||
if isinstance(body, str):
|
||||
body = json.loads(body)
|
||||
|
||||
return WebhookPayloadWebhookDto.from_dict(body)
|
||||
return WebhookPayloadDto.from_dict(body)
|
||||
|
||||
@staticmethod
|
||||
def is_user_event(event: str) -> bool:
|
||||
return event in {e.value for e in UserEvent}
|
||||
"""Check if event is a user event."""
|
||||
return event.startswith("user.")
|
||||
|
||||
@staticmethod
|
||||
def is_user_hwid_devices_event(event: str) -> bool:
|
||||
"""Check if event is a user HWID devices event."""
|
||||
return event.startswith("user_hwid_devices.")
|
||||
|
||||
@staticmethod
|
||||
def is_node_event(event: str) -> bool:
|
||||
return event in {e.value for e in NodeEvent}
|
||||
"""Check if event is a node event."""
|
||||
return event.startswith("node.")
|
||||
|
||||
@staticmethod
|
||||
def is_infra_billing_event(event: str) -> bool:
|
||||
return event in {e.value for e in InfraBillingEvent}
|
||||
"""Check if event is an infra billing event."""
|
||||
return event.startswith("crm.infra_billing")
|
||||
|
||||
@staticmethod
|
||||
def is_crm_event(event: str) -> bool:
|
||||
"""Check if event is a CRM event."""
|
||||
return event.startswith("crm.")
|
||||
|
||||
@staticmethod
|
||||
def is_service_event(event: str) -> bool:
|
||||
return event in {e.value for e in ServiceEvent}
|
||||
"""Check if event is a service event."""
|
||||
return event.startswith("service.")
|
||||
|
||||
@staticmethod
|
||||
def is_errors_event(event: str) -> bool:
|
||||
"""Check if event is an errors event."""
|
||||
return event.startswith("errors.")
|
||||
|
||||
@staticmethod
|
||||
def get_typed_data(payload: WebhookPayloadDto) -> Union[UserDto, NodesDto, HwidUserDeviceDto, LoginAttemptDto, UserHwidDeviceEventDto, dict]:
|
||||
"""
|
||||
Get typed data from webhook payload based on event type.
|
||||
|
||||
:param payload: Parsed webhook payload.
|
||||
:return: Typed data object.
|
||||
"""
|
||||
return payload.data
|
||||
|
||||
@staticmethod
|
||||
def extract_user_hwid_event_data(payload: WebhookPayloadDto) -> Optional[tuple[UserDto, HwidUserDeviceDto]]:
|
||||
"""
|
||||
Extract user and HWID device from user_hwid_devices event.
|
||||
|
||||
:param payload: Parsed webhook payload.
|
||||
:return: Tuple of (UserDto, HwidUserDeviceDto) or None if not a HWID event.
|
||||
"""
|
||||
if not WebhookUtility.is_user_hwid_devices_event(payload.event):
|
||||
return None
|
||||
|
||||
if isinstance(payload.data, dict):
|
||||
user_data = payload.data.get("user", {})
|
||||
hwid_data = payload.data.get("hwidUserDevice", {})
|
||||
|
||||
return (
|
||||
UserDto(**user_data),
|
||||
HwidUserDeviceDto(**hwid_data)
|
||||
)
|
||||
|
||||
return None
|
||||
|
|
@ -5,8 +5,9 @@ from .fingerprint import Fingerprint
|
|||
from .security_layer import SecurityLayer
|
||||
from .template_type import TemplateType
|
||||
from .users import TrafficLimitStrategy, UserStatus
|
||||
from .webhook import UserEvent, NodeEvent, InfraBillingEvent, ServiceEvent
|
||||
|
||||
from .webhook import (
|
||||
TCRMEvents, TErrorsEvents, TNodeEvents, TResetPeriods, TServiceEvents, TUserEvents, TUserHwidDevicesEvents, TUsersStatus
|
||||
)
|
||||
__all__ = [
|
||||
"TrafficLimitStrategy",
|
||||
"UserStatus",
|
||||
|
|
@ -15,9 +16,14 @@ __all__ = [
|
|||
"ALPN",
|
||||
"Fingerprint",
|
||||
"SecurityLayer",
|
||||
"UserEvent",
|
||||
"NodeEvent",
|
||||
"InfraBillingEvent",
|
||||
"ServiceEvent",
|
||||
"TemplateType",
|
||||
# Webhook enums
|
||||
"TNodeEvents",
|
||||
"TUserEvents",
|
||||
"TServiceEvents",
|
||||
"TErrorsEvents",
|
||||
"TCRMEvents",
|
||||
"TUserHwidDevicesEvents",
|
||||
"TResetPeriods",
|
||||
"TUsersStatus",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,46 +1,60 @@
|
|||
from typing import Literal
|
||||
|
||||
from enum import StrEnum
|
||||
# ---------------- ENUMS / CONSTANTS ---------------- #
|
||||
|
||||
class UserEvent(StrEnum):
|
||||
CREATED = "user.created"
|
||||
MODIFIED = "user.modified"
|
||||
DELETED = "user.deleted"
|
||||
REVOKED = "user.revoked"
|
||||
DISABLED = "user.disabled"
|
||||
ENABLED = "user.enabled"
|
||||
LIMITED = "user.limited"
|
||||
EXPIRED = "user.expired"
|
||||
TRAFFIC_RESET = "user.traffic_reset"
|
||||
EXPIRES_IN_72_HOURS = "user.expires_in_72_hours"
|
||||
EXPIRES_IN_48_HOURS = "user.expires_in_48_hours"
|
||||
EXPIRES_IN_24_HOURS = "user.expires_in_24_hours"
|
||||
EXPIRED_24_HOURS_AGO = "user.expired_24_hours_ago"
|
||||
FIRST_CONNECTED = "user.first_connected"
|
||||
BANDWIDTH_USAGE_THRESHOLD_REACHED = "user.bandwidth_usage_threshold_reached"
|
||||
TNodeEvents = Literal[
|
||||
"node.created",
|
||||
"node.modified",
|
||||
"node.disabled",
|
||||
"node.enabled",
|
||||
"node.deleted",
|
||||
"node.connection_lost",
|
||||
"node.connection_restored",
|
||||
"node.traffic_notify",
|
||||
]
|
||||
|
||||
TUserEvents = Literal[
|
||||
"user.created",
|
||||
"user.modified",
|
||||
"user.deleted",
|
||||
"user.revoked",
|
||||
"user.disabled",
|
||||
"user.enabled",
|
||||
"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",
|
||||
]
|
||||
|
||||
class NodeEvent(StrEnum):
|
||||
CREATED = "node.created"
|
||||
MODIFIED = "node.modified"
|
||||
DISABLED = "node.disabled"
|
||||
ENABLED = "node.enabled"
|
||||
DELETED = "node.deleted"
|
||||
CONNECTION_LOST = "node.connection_lost"
|
||||
CONNECTION_RESTORED = "node.connection_restored"
|
||||
TRAFFIC_NOTIFY = "node.traffic_notify"
|
||||
TServiceEvents = Literal[
|
||||
"service.panel_started",
|
||||
"service.login_attempt_failed",
|
||||
"service.login_attempt_success",
|
||||
]
|
||||
|
||||
TErrorsEvents = Literal[
|
||||
"errors.bandwidth_usage_threshold_reached_max_notifications",
|
||||
]
|
||||
|
||||
class InfraBillingEvent(StrEnum):
|
||||
PAYMENT_IN_7_DAYS = "crm.infra_billing_node_payment_in_7_days"
|
||||
PAYMENT_IN_48HRS = "crm.infra_billing_node_payment_in_48hrs"
|
||||
PAYMENT_IN_24HRS = "crm.infra_billing_node_payment_in_24hrs"
|
||||
PAYMENT_DUE_TODAY = "crm.infra_billing_node_payment_due_today"
|
||||
PAYMENT_OVERDUE_24HRS = "crm.infra_billing_node_payment_overdue_24hrs"
|
||||
PAYMENT_OVERDUE_48HRS = "crm.infra_billing_node_payment_overdue_48hrs"
|
||||
PAYMENT_OVERDUE_7_DAYS = "crm.infra_billing_node_payment_overdue_7_days"
|
||||
TCRMEvents = Literal[
|
||||
"crm.infra_billing_node_payment_in_7_days",
|
||||
"crm.infra_billing_node_payment_in_48hrs",
|
||||
"crm.infra_billing_node_payment_in_24hrs",
|
||||
"crm.infra_billing_node_payment_due_today",
|
||||
"crm.infra_billing_node_payment_overdue_24hrs",
|
||||
"crm.infra_billing_node_payment_overdue_48hrs",
|
||||
"crm.infra_billing_node_payment_overdue_7_days",
|
||||
]
|
||||
|
||||
TUserHwidDevicesEvents = Literal[
|
||||
"user_hwid_devices.added",
|
||||
"user_hwid_devices.deleted",
|
||||
]
|
||||
|
||||
class ServiceEvent(StrEnum):
|
||||
PANEL_STARTED = "service.panel_started"
|
||||
LOGIN_ATTEMPT_FAILED = "service.login_attempt_failed"
|
||||
LOGIN_ATTEMPT_SUCCESS = "service.login_attempt_success"
|
||||
TResetPeriods = Literal["NO_RESET", "DAY", "WEEK", "MONTH"]
|
||||
TUsersStatus = Literal["DISABLED", "LIMITED", "EXPIRED", "ACTIVE"]
|
||||
|
|
|
|||
|
|
@ -1,175 +1,268 @@
|
|||
from datetime import datetime
|
||||
from typing import List, Optional, Union, Literal
|
||||
from typing import List, Optional, Literal, Union
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic.alias_generators import to_camel
|
||||
from remnawave.enums import (
|
||||
TUsersStatus, TUserEvents, TUserHwidDevicesEvents, TServiceEvents, TNodeEvents, TErrorsEvents, TCRMEvents, TResetPeriods
|
||||
)
|
||||
# ---------------- USER ---------------- #
|
||||
|
||||
class LastConnectedNodeDto(BaseModel):
|
||||
node_name: str
|
||||
country_code: str
|
||||
connected_at: datetime
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class InboundWebhookDto(BaseModel):
|
||||
class InternalSquadDto(BaseModel):
|
||||
uuid: UUID
|
||||
tag: str
|
||||
type: str
|
||||
network: str | None = None
|
||||
security: str | None = None
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class WebhookUserWebhookDto(BaseModel):
|
||||
class BaseUserDto(BaseModel):
|
||||
uuid: UUID
|
||||
subscription_uuid: UUID
|
||||
short_uuid: str
|
||||
username: str
|
||||
status: Literal['DISABLED', 'LIMITED', 'EXPIRED', 'ACTIVE']
|
||||
status: TUsersStatus
|
||||
|
||||
used_traffic_bytes: str
|
||||
lifetime_used_traffic_bytes: str
|
||||
|
||||
traffic_limit_bytes: str
|
||||
traffic_limit_strategy: Literal['NO_RESET', 'DAY', 'WEEK', 'MONTH']
|
||||
sub_last_user_agent: str | None = None
|
||||
sub_last_opened_at: datetime | None = None
|
||||
traffic_limit_strategy: TResetPeriods
|
||||
sub_last_user_agent: Optional[str] = None
|
||||
sub_last_opened_at: Optional[datetime] = None
|
||||
|
||||
expire_at: datetime
|
||||
online_at: datetime | None = None
|
||||
sub_revoked_at: datetime | None = None
|
||||
last_traffic_reset_at: datetime | None = None
|
||||
sub_revoked_at: Optional[datetime] = None
|
||||
last_traffic_reset_at: Optional[datetime] = None
|
||||
|
||||
trojan_password: str
|
||||
vless_uuid: UUID
|
||||
ss_password: str
|
||||
description: str | None = None
|
||||
telegram_id: str | None = None
|
||||
email: str | None = None
|
||||
hwid_device_limit: int | None = None
|
||||
|
||||
description: Optional[str] = None
|
||||
tag: Optional[str] = None
|
||||
telegram_id: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
|
||||
hwid_device_limit: Optional[int] = None
|
||||
|
||||
first_connected_at: Optional[datetime] = None
|
||||
last_triggered_threshold: int
|
||||
|
||||
online_at: Optional[datetime] = None
|
||||
last_connected_node_uuid: Optional[str] = None
|
||||
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
first_connected_at: datetime | None = None
|
||||
last_triggered_threshold: int
|
||||
active_user_inbounds: List[InboundWebhookDto]
|
||||
|
||||
model_config = {
|
||||
"alias_generator": to_camel,
|
||||
"populate_by_name": True,
|
||||
}
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class WebhookNodeWebhookDto(BaseModel):
|
||||
class UserDto(BaseUserDto):
|
||||
active_internal_squads: List[InternalSquadDto] = Field(default_factory=list)
|
||||
last_connected_node: Optional[LastConnectedNodeDto] = None
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class UserEventDto(BaseModel):
|
||||
user: UserDto
|
||||
event_name: TUserEvents
|
||||
skip_telegram_notification: bool = False
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
# ---------------- HWID DEVICES ---------------- #
|
||||
|
||||
class HwidUserDeviceDto(BaseModel):
|
||||
hwid: str
|
||||
user_uuid: UUID
|
||||
platform: Optional[str] = None
|
||||
os_version: Optional[str] = None
|
||||
device_model: Optional[str] = None
|
||||
user_agent: Optional[str] = None
|
||||
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class UserHwidDeviceEventDto(BaseModel):
|
||||
data: dict
|
||||
event_name: TUserHwidDevicesEvents
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
@classmethod
|
||||
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):
|
||||
username: str
|
||||
ip: str
|
||||
user_agent: str
|
||||
description: Optional[str] = None
|
||||
password: Optional[str] = None
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class ServiceEventDto(BaseModel):
|
||||
event_name: TServiceEvents
|
||||
data: dict
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
# ---------------- NODE ENTITIES ---------------- #
|
||||
|
||||
class ConfigProfileInboundDto(BaseModel):
|
||||
uuid: UUID
|
||||
name: str
|
||||
config: dict
|
||||
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class InfraProviderDto(BaseModel):
|
||||
name: str
|
||||
uuid: UUID
|
||||
favicon_link: Optional[str] = None
|
||||
login_url: Optional[str] = None
|
||||
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
billing_history: Optional[dict] = None
|
||||
billing_nodes: Optional[List[dict]] = None
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class NodesDto(BaseModel):
|
||||
uuid: UUID
|
||||
name: str
|
||||
address: str
|
||||
port: int | None = None
|
||||
port: Optional[int] = None
|
||||
is_connected: bool
|
||||
is_connecting: bool
|
||||
is_disabled: bool
|
||||
is_node_online: bool
|
||||
is_xray_running: bool
|
||||
last_status_change: datetime | None = None
|
||||
last_status_message: str | None = None
|
||||
xray_version: str | None = None
|
||||
|
||||
last_status_change: Optional[datetime] = None
|
||||
last_status_message: Optional[str] = None
|
||||
|
||||
xray_version: Optional[str] = None
|
||||
node_version: Optional[str] = None
|
||||
xray_uptime: str
|
||||
users_online: int | None = None
|
||||
|
||||
users_online: Optional[int] = None
|
||||
|
||||
is_traffic_tracking_active: bool
|
||||
traffic_reset_day: int | None = None
|
||||
traffic_limit_bytes: str | None = None
|
||||
traffic_used_bytes: str | None = None
|
||||
notify_percent: int | None = None
|
||||
traffic_reset_day: Optional[int] = None
|
||||
traffic_limit_bytes: Optional[str] = None
|
||||
traffic_used_bytes: Optional[str] = None
|
||||
notify_percent: Optional[int] = None
|
||||
|
||||
view_position: int
|
||||
country_code: str
|
||||
consumption_multiplier: str
|
||||
cpu_count: int | None = None
|
||||
cpu_model: str | None = None
|
||||
total_ram: str | None = None
|
||||
|
||||
cpu_count: Optional[int] = None
|
||||
cpu_model: Optional[str] = None
|
||||
total_ram: Optional[str] = None
|
||||
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
excluded_inbounds: List[InboundWebhookDto]
|
||||
|
||||
model_config = {
|
||||
"alias_generator": to_camel,
|
||||
"populate_by_name": True,
|
||||
}
|
||||
active_config_profile_uuid: Optional[UUID] = None
|
||||
active_inbounds: List[ConfigProfileInboundDto] = Field(default_factory=list)
|
||||
|
||||
provider_uuid: Optional[UUID] = None
|
||||
provider: Optional[InfraProviderDto] = None
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class InfraBillingSummaryWebhookDto(BaseModel):
|
||||
node_name: str
|
||||
provider_name: str
|
||||
login_url: str
|
||||
next_billing_at: datetime
|
||||
class NodeEventDto(BaseModel):
|
||||
node: NodesDto
|
||||
event_name: TNodeEvents
|
||||
|
||||
model_config = {
|
||||
"alias_generator": to_camel,
|
||||
"populate_by_name": True,
|
||||
}
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class LoginAttemptWebhookDto(BaseModel):
|
||||
username: str
|
||||
ip: str
|
||||
user_agent: str
|
||||
description: str | None = None
|
||||
password: str | None = None
|
||||
# ---------------- ERROR EVENTS ---------------- #
|
||||
|
||||
model_config = {
|
||||
"alias_generator": to_camel,
|
||||
"populate_by_name": True,
|
||||
}
|
||||
class CustomErrorEventDto(BaseModel):
|
||||
event_name: TErrorsEvents
|
||||
data: dict
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class WebhookServiceWebhookDto(BaseModel):
|
||||
login_attempt: LoginAttemptWebhookDto | None = None
|
||||
# ---------------- CRM EVENTS ---------------- #
|
||||
|
||||
model_config = {
|
||||
"alias_generator": to_camel,
|
||||
"populate_by_name": True,
|
||||
}
|
||||
class CrmEventDto(BaseModel):
|
||||
event_name: TCRMEvents
|
||||
data: dict
|
||||
skip_telegram_notification: bool = False
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
|
||||
class WebhookHeadersWebhookDto(BaseModel):
|
||||
signature: str = Field(alias="x-remnawave-signature")
|
||||
timestamp: str = Field(alias="x-remnawave-timestamp")
|
||||
# ---------------- WEBHOOK PAYLOAD ---------------- #
|
||||
|
||||
model_config = {
|
||||
"populate_by_name": True,
|
||||
"extra": "allow",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_headers(cls, headers: dict[str, str]) -> "WebhookHeadersWebhookDto":
|
||||
"""
|
||||
Creates a WebhookHeadersWebhookDto from a dictionary of HTTP headers.
|
||||
Handles case-insensitive keys.
|
||||
"""
|
||||
normalized = {k.lower(): v for k, v in headers.items()}
|
||||
return cls(
|
||||
**{
|
||||
"x-remnawave-signature": normalized.get("x-remnawave-signature"),
|
||||
"x-remnawave-timestamp": normalized.get("x-remnawave-timestamp"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class WebhookPayloadWebhookDto(BaseModel):
|
||||
class WebhookPayloadDto(BaseModel):
|
||||
event: str
|
||||
data: Union[
|
||||
WebhookUserWebhookDto,
|
||||
WebhookNodeWebhookDto,
|
||||
InfraBillingSummaryWebhookDto,
|
||||
WebhookServiceWebhookDto,
|
||||
UserDto,
|
||||
NodesDto,
|
||||
HwidUserDeviceDto,
|
||||
LoginAttemptDto,
|
||||
UserHwidDeviceEventDto,
|
||||
dict
|
||||
]
|
||||
timestamp: datetime
|
||||
|
||||
model_config = {"alias_generator": to_camel, "populate_by_name": True}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, payload: dict) -> "WebhookPayloadWebhookDto":
|
||||
"""
|
||||
Parses the webhook payload and automatically determines the WebhookDto based on the event type.
|
||||
"""
|
||||
def from_dict(cls, payload: dict) -> "WebhookPayloadDto":
|
||||
event = payload.get("event", "")
|
||||
data_raw = payload.get("data", {})
|
||||
|
||||
if event.startswith("user."):
|
||||
data = WebhookUserWebhookDto(**data_raw)
|
||||
data = UserDto(**data_raw)
|
||||
elif event.startswith("user_hwid_devices."):
|
||||
data = HwidUserDeviceDto(**data_raw)
|
||||
elif event.startswith("node."):
|
||||
data = WebhookNodeWebhookDto(**data_raw)
|
||||
elif event.startswith("crm.infra_billing"):
|
||||
data = InfraBillingSummaryWebhookDto(**data_raw)
|
||||
data = NodesDto(**data_raw)
|
||||
elif event.startswith("service."):
|
||||
data = WebhookServiceWebhookDto(**data_raw)
|
||||
# может быть loginAttempt или другое
|
||||
if "username" in data_raw and "ip" in data_raw:
|
||||
data = LoginAttemptDto(**data_raw)
|
||||
else:
|
||||
data = data_raw
|
||||
elif event.startswith("errors."):
|
||||
data = data_raw
|
||||
elif event.startswith("crm."):
|
||||
data = data_raw
|
||||
else:
|
||||
data = data_raw
|
||||
|
||||
|
|
@ -179,8 +272,4 @@ class WebhookPayloadWebhookDto(BaseModel):
|
|||
else:
|
||||
timestamp = timestamp_raw
|
||||
|
||||
return cls(
|
||||
event=event,
|
||||
data=data,
|
||||
timestamp=timestamp
|
||||
)
|
||||
return cls(event=event, data=data, timestamp=timestamp)
|
||||
Loading…
Add table
Add a link
Reference in a new issue