8375d7ecc5
- Multi-channel subscription enforcement via middleware, events, and cabinet API - 3-layer cache architecture: Redis -> PostgreSQL -> rate-limited Telegram API - ChatMemberUpdated event-driven tracking with automatic VPN access control - Admin management via bot FSM handler and REST API with full CRUD - Channel ID normalization: @username resolved to numeric ID at creation time - Fail-closed error handling: API errors deny access (security-first) - Background reconciliation with keyset pagination (100 per batch) - Per-user rate limiting on subscription check button (5s cooldown) - Redis connection pooling via cache singleton (no per-request connections) - Database: channel_id index, multi-row upsert optimization - Localization: en, ru, zh, fa, ua translations for all new strings - Frontend blocking UI with channel list and subscription status - Admin channel management page with toggle, delete, and create
79 lines
2.2 KiB
Python
79 lines
2.2 KiB
Python
"""Pydantic v2 schemas for channel subscription management."""
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
|
|
from app.database.crud.required_channel import validate_channel_id as _validate_channel_id_format
|
|
|
|
|
|
def _validate_channel_link_value(v: str | None) -> str | None:
|
|
"""Shared channel_link validation: t.me URL, @username auto-convert, http->https upgrade."""
|
|
if v is None:
|
|
return v
|
|
v = v.strip()
|
|
if v.startswith('http://t.me/'):
|
|
v = v.replace('http://', 'https://', 1)
|
|
if v.startswith('https://t.me/'):
|
|
return v
|
|
if v.startswith('@'):
|
|
return f'https://t.me/{v[1:]}'
|
|
raise ValueError('channel_link must be a t.me URL or @username')
|
|
|
|
|
|
class ChannelResponse(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
channel_id: str
|
|
channel_link: str | None
|
|
title: str | None
|
|
is_active: bool
|
|
sort_order: int
|
|
|
|
|
|
class ChannelListResponse(BaseModel):
|
|
items: list[ChannelResponse]
|
|
total: int
|
|
|
|
|
|
class ChannelCreateRequest(BaseModel):
|
|
channel_id: str
|
|
channel_link: str | None = None
|
|
title: str | None = Field(None, max_length=255)
|
|
|
|
@field_validator('channel_id')
|
|
@classmethod
|
|
def validate_channel_id(cls, v: str) -> str:
|
|
return _validate_channel_id_format(v)
|
|
|
|
@field_validator('channel_link')
|
|
@classmethod
|
|
def validate_channel_link(cls, v: str | None) -> str | None:
|
|
return _validate_channel_link_value(v)
|
|
|
|
|
|
class ChannelUpdateRequest(BaseModel):
|
|
channel_id: str | None = None
|
|
channel_link: str | None = None
|
|
title: str | None = Field(None, max_length=255)
|
|
is_active: bool | None = None
|
|
sort_order: int | None = None
|
|
|
|
@field_validator('channel_id')
|
|
@classmethod
|
|
def validate_channel_id(cls, v: str | None) -> str | None:
|
|
if v is None:
|
|
return v
|
|
return _validate_channel_id_format(v)
|
|
|
|
@field_validator('channel_link')
|
|
@classmethod
|
|
def validate_channel_link(cls, v: str | None) -> str | None:
|
|
return _validate_channel_link_value(v)
|
|
|
|
|
|
class ChannelSubscriptionStatus(BaseModel):
|
|
channel_id: str
|
|
channel_link: str | None
|
|
title: str | None
|
|
is_subscribed: bool
|