from datetime import datetime from typing import Optional from pydantic import BaseModel, Field, field_validator, model_validator class HookCreate(BaseModel): message: str = Field(..., min_length=1, description="Message body supporting Markdown") chat_id: str = Field(..., min_length=1, description="Target chat ID or username") class HookRead(BaseModel): hook_id: str message: str chat_id: str created_at: datetime last_triggered_at: Optional[datetime] = None @property def action_path(self) -> str: return f"/action/{self.hook_id}" class HookResponse(HookRead): action_url: str class HookUpdate(BaseModel): hook_id: Optional[str] = Field( default=None, min_length=3, max_length=64, pattern=r"^[A-Za-z0-9_-]+$", description="New hook identifier", ) chat_id: Optional[str] = Field( default=None, min_length=1, description="Updated target chat ID or username", ) message: Optional[str] = Field( default=None, min_length=1, description="Updated message body supporting Markdown", ) @model_validator(mode="after") def ensure_any_field(cls, values: "HookUpdate") -> "HookUpdate": if values.hook_id is None and values.chat_id is None and values.message is None: raise ValueError("Provide at least one field to update") return values @field_validator("hook_id", mode="before") @classmethod def normalize_hook_id(cls, value: Optional[str]) -> Optional[str]: if value is None: return None trimmed = value.strip() if not trimmed: raise ValueError("Hook ID cannot be empty") return trimmed @field_validator("chat_id", mode="before") @classmethod def normalize_chat_id(cls, value: Optional[str]) -> Optional[str]: if value is None: return None trimmed = value.strip() if not trimmed: raise ValueError("Chat ID cannot be empty") return trimmed @field_validator("message", mode="before") @classmethod def normalize_message(cls, value: Optional[str]) -> Optional[str]: if value is None: return None stripped = value.strip() if not stripped: raise ValueError("Message cannot be empty") return stripped class LoginStartRequest(BaseModel): phone_number: Optional[str] = None class LoginVerifyRequest(BaseModel): code: str phone_number: Optional[str] = None password: Optional[str] = None class MessageTriggerResponse(BaseModel): status: str hook_id: str chat_id: str class StatusResponse(BaseModel): authorized: bool user: Optional[str] session_active: bool phone_number: Optional[str] code_sent: bool = False class RecentChat(BaseModel): chat_id: str display_name: str chat_type: str username: Optional[str] = None phone_number: Optional[str] = None last_used_at: Optional[datetime] = None