From 45a5b59cc827f7a007bef6bb3feae2373d8b1eec Mon Sep 17 00:00:00 2001 From: Artem Tsyrulnikov Date: Tue, 23 Dec 2025 10:11:31 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=8F=D0=B2=D0=BD=D0=BE=20=D1=83=D0=BA?= =?UTF-8?q?=D0=B0=D0=B7=D0=B0=D0=BD=D1=8B=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81?= =?UTF-8?q?=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D0=B8=20=D0=BA=20Protocol=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B5=D1=85=D0=BE=D0=B4?= =?UTF-8?q?=D0=B0=20=D0=BA=20=D0=B8=D0=BC=D0=BF=D0=BB=D0=B5=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/jwt_base.py | 12 ++++++++++++ src/adapter/jwt.py | 12 +++--------- src/adapter/parser.py | 15 +++------------ src/adapter/postgres.py | 3 ++- src/adapter/telegram_bot.py | 3 ++- src/config.py | 2 +- .../telegram_callback/chat_join_request.py | 5 +---- .../telegram_callback/chat_member_updated.py | 8 ++++---- src/main.py | 2 +- src/usecase/__init__.py | 4 ++-- .../attach_channel_to_purchase_plan.py | 2 +- 11 files changed, 32 insertions(+), 36 deletions(-) create mode 100644 shared/jwt_base.py diff --git a/shared/jwt_base.py b/shared/jwt_base.py new file mode 100644 index 0000000..99951eb --- /dev/null +++ b/shared/jwt_base.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel + + +class JWTConfig(BaseModel): + SECRET_KEY: str + ALGORITHM: str = 'HS256' + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 days + + +class JWTBase: + def __init__(self, config: JWTConfig) -> None: + self.config = config \ No newline at end of file diff --git a/src/adapter/jwt.py b/src/adapter/jwt.py index 12ee35d..7f86391 100644 --- a/src/adapter/jwt.py +++ b/src/adapter/jwt.py @@ -4,11 +4,8 @@ import uuid import jwt import pydantic - -class JWTConfig(pydantic.BaseModel): - SECRET_KEY: str - ALGORITHM: str = 'HS256' - ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 days +from shared.jwt_base import JWTBase +from src.usecase import JWTEncoder class JWTPayload(pydantic.BaseModel): @@ -17,10 +14,7 @@ class JWTPayload(pydantic.BaseModel): username: str | None -class JWT: - def __init__(self, config: JWTConfig) -> None: - self.config = config - +class JWT(JWTBase, JWTEncoder): def encode_access_token( self, user_id: uuid.UUID, diff --git a/src/adapter/parser.py b/src/adapter/parser.py index 28367b7..fa2dde2 100644 --- a/src/adapter/parser.py +++ b/src/adapter/parser.py @@ -4,6 +4,8 @@ from typing import Any import httpx from pydantic import BaseModel +from src.usecase import Parser + log = logging.getLogger(__name__) @@ -16,23 +18,12 @@ class FetchChannelResponse(BaseModel): pts: int -class ParserClient: +class ParserClient(Parser): def __init__(self, base_url: str, timeout: float = 5.0): self.base_url = base_url.rstrip('/') self.timeout = timeout async def fetch_telegram_channel(self, username: str) -> FetchChannelResponse | None: - """ - Проверяет существование канала в Telegram и получает его метаданные. - - Returns: - FetchChannelResponse если канал найден - None если канал не найден (404) - - Raises: - httpx.HTTPError для других ошибок (таймаут, network error, 5xx) - """ - # Убираем @ если пользователь его указал username = username.lstrip('@') url = f'{self.base_url}/fetch-telegram-channel' diff --git a/src/adapter/postgres.py b/src/adapter/postgres.py index 8c1b078..caa82f1 100644 --- a/src/adapter/postgres.py +++ b/src/adapter/postgres.py @@ -8,11 +8,12 @@ from tortoise.transactions import in_transaction from shared.datebase_base import DatabaseBase from src import domain +from usecase import Database log = logging.getLogger(__name__) -class Postgres(DatabaseBase): +class Postgres(DatabaseBase, Database): def transaction(self) -> typing.AsyncContextManager[None]: return in_transaction() diff --git a/src/adapter/telegram_bot.py b/src/adapter/telegram_bot.py index bed54e9..f37ea3b 100644 --- a/src/adapter/telegram_bot.py +++ b/src/adapter/telegram_bot.py @@ -3,11 +3,12 @@ import logging from aiogram.types import BotCommand, InlineKeyboardButton, InlineKeyboardMarkup from shared.telegram_base import TelegramBase +from usecase import TelegramBotWriter log = logging.getLogger(__name__) -class TelegramBot(TelegramBase): +class TelegramBot(TelegramBase, TelegramBotWriter): async def send_message(self, text: str, chat_id: int) -> None: await self.bot.send_message(chat_id=chat_id, text=text) diff --git a/src/config.py b/src/config.py index 9ff6ff6..c92b739 100644 --- a/src/config.py +++ b/src/config.py @@ -3,9 +3,9 @@ from pydantic_settings import BaseSettings, SettingsConfigDict from shared.config_helper import load_settings from shared.datebase_base import DatabaseConfig +from shared.jwt_base import JWTConfig from shared.logger import LoggerConfig from shared.telegram_base import TelegramConfig -from src.adapter.jwt import JWTConfig class AppConfig(BaseModel): diff --git a/src/controller/telegram_callback/chat_join_request.py b/src/controller/telegram_callback/chat_join_request.py index ab5bbec..176fb79 100644 --- a/src/controller/telegram_callback/chat_join_request.py +++ b/src/controller/telegram_callback/chat_join_request.py @@ -17,15 +17,12 @@ async def on_chat_join_request(event: ChatJoinRequest) -> None: log.error('Failed to get user data from chat join request') return - # Получаем invite_link, по которой пользователь отправил запрос invite_link = event.invite_link if not invite_link or not invite_link.invite_link: log.debug('No invite_link in chat join request event') return - usecase = deps.get_usecase() - - await usecase.handle_subscription( + await deps.get_usecase().handle_subscription( user_telegram_id=event.from_user.id, username=event.from_user.username, invite_link=invite_link.invite_link, diff --git a/src/controller/telegram_callback/chat_member_updated.py b/src/controller/telegram_callback/chat_member_updated.py index 978cf2e..84d94e2 100644 --- a/src/controller/telegram_callback/chat_member_updated.py +++ b/src/controller/telegram_callback/chat_member_updated.py @@ -30,17 +30,17 @@ async def on_chat_member_updated(event: ChatMemberUpdated) -> None: is_now_not_member = new_status in ('left', 'kicked') user_left = was_member and is_now_not_member - usecase = deps.get_usecase() - if user_joined: invite_link = event.invite_link.invite_link if event.invite_link else None if not invite_link: log.debug('No invite_link in chat member update event') return - await usecase.handle_subscription( + await deps.get_usecase().handle_subscription( user_telegram_id=event.from_user.id, username=event.from_user.username, invite_link=invite_link ) elif user_left: - await usecase.handle_unsubscription(user_telegram_id=event.from_user.id, channel_telegram_id=event.chat.id) + await deps.get_usecase().handle_unsubscription( + user_telegram_id=event.from_user.id, channel_telegram_id=event.chat.id + ) diff --git a/src/main.py b/src/main.py index df2f472..d800ec4 100644 --- a/src/main.py +++ b/src/main.py @@ -44,7 +44,7 @@ usecase = Usecase( database=postgres, telegram_bot=telegram, jwt_encoder=jwt_encoder, - parser_client=parser_client, + parser=parser_client, ) deps.set_usecase(usecase) diff --git a/src/usecase/__init__.py b/src/usecase/__init__.py index b3162b7..684eed5 100644 --- a/src/usecase/__init__.py +++ b/src/usecase/__init__.py @@ -296,7 +296,7 @@ class FetchChannelResponse(typing.Protocol): title: str -class ParserClient(typing.Protocol): +class Parser(typing.Protocol): async def fetch_telegram_channel(self, username: str) -> FetchChannelResponse | None: ... @@ -305,7 +305,7 @@ class Usecase: database: Database telegram_bot: TelegramBotWriter jwt_encoder: JWTEncoder - parser_client: ParserClient + parser: Parser async def ensure_workspace_access(self, workspace_id: UUID, user_id: UUID) -> domain.Workspace: workspace = await self.database.get_workspace_for_user(workspace_id, user_id) diff --git a/src/usecase/purchase_plan/attach_channel_to_purchase_plan.py b/src/usecase/purchase_plan/attach_channel_to_purchase_plan.py index 0e96b87..e1c4cba 100644 --- a/src/usecase/purchase_plan/attach_channel_to_purchase_plan.py +++ b/src/usecase/purchase_plan/attach_channel_to_purchase_plan.py @@ -30,7 +30,7 @@ async def attach_channel_to_purchase_plan( channel = await self.database.get_channel(username=input.username) if not channel: - parser_response = await self.parser_client.fetch_telegram_channel(input.username) + parser_response = await self.parser.fetch_telegram_channel(input.username) if not parser_response: raise domain.TelegramChannelNotFound(input.username)