feat: /new_creative tg_bot
This commit is contained in:
176
CLAUDE.md
176
CLAUDE.md
@@ -84,10 +84,15 @@ The codebase follows a clean architecture pattern with clear separation of conce
|
||||
|
||||
2. **Use Case Layer** (`src/usecase/`)
|
||||
- Business logic functions organized by feature
|
||||
- Each use case is a standalone function
|
||||
- Depends on Protocol interfaces (Database, TelegramWriter, JWTEncoder)
|
||||
- Each use case is a standalone function with signature: `async def use_case_name(self: 'Usecase', ...) -> ReturnType`
|
||||
- Depends on Protocol interfaces (Database, TelegramBotWriter, JWTEncoder)
|
||||
- Use cases are assembled in the `Usecase` dataclass for dependency injection
|
||||
- Один use case отвечает за полноценный сценарий «от триггера до завершения» и не должен вызывать другие use case'ы напрямую (общую логику выносить в хелперы/сервисы внутри того же модуля)
|
||||
- **IMPORTANT RULES:**
|
||||
- Один use case отвечает за полноценный сценарий «от триггера до завершения»
|
||||
- Use case НЕ должен вызывать другие use cases (общую логику выносить в хелперы/сервисы)
|
||||
- Use case содержит ВСЮ бизнес-логику, включая управление состояниями диалога
|
||||
- Use case может отправлять сообщения пользователям через `self.telegram_bot` protocol
|
||||
- Всегда оборачивать операции БД в `async with self.database.transaction()`
|
||||
|
||||
3. **Adapter Layer** (`src/adapter/`)
|
||||
- Concrete implementations of protocol interfaces:
|
||||
@@ -98,7 +103,15 @@ The codebase follows a clean architecture pattern with clear separation of conce
|
||||
4. **Controller Layer** (`src/controller/`)
|
||||
- **HTTP controllers** (`src/controller/http/`): FastAPI route handlers
|
||||
- **Telegram callbacks** (`src/controller/telegram_callback/`): Telegram event handlers
|
||||
- Controllers call use cases to perform business logic
|
||||
- **ВАЖНО: Controllers - это тонкий слой роутинга**
|
||||
- Контроллер только извлекает данные из событий (HTTP request / Telegram update)
|
||||
- Валидирует базовые параметры (user exists, message not empty)
|
||||
- Вызывает соответствующий use case
|
||||
- НЕ содержит бизнес-логику
|
||||
- НЕ обращается к базе данных напрямую
|
||||
- НЕ отправляет сообщения пользователям
|
||||
- Все handlers регистрируются через декораторы на `telegram_callback_router`
|
||||
- Handlers импортируются в `__init__.py` для auto-регистрации
|
||||
|
||||
5. **DTO Layer** (`src/dto/`)
|
||||
- Pydantic models for request/response validation
|
||||
@@ -168,3 +181,158 @@ The codebase follows a clean architecture pattern with clear separation of conce
|
||||
- Line length: 120 characters
|
||||
- Python 3.13+ syntax
|
||||
- Strict mypy typing enabled
|
||||
|
||||
### Telegram Bot Patterns
|
||||
|
||||
**State Management:**
|
||||
- Состояния хранятся в БД через модель `TelegramState`
|
||||
- Используется `TelegramStateEnum` для типизации состояний (не строки!)
|
||||
- Database protocol методы: `get_telegram_state()`, `set_telegram_state()`, `clear_telegram_state()`
|
||||
|
||||
**Controller Pattern (Telegram):**
|
||||
```python
|
||||
from aiogram import F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message
|
||||
|
||||
@telegram_callback_router.message(Command('command_name'))
|
||||
async def cmd_handler(message: Message) -> None:
|
||||
if not message.from_user:
|
||||
log.error('Failed to get user data')
|
||||
return
|
||||
|
||||
usecase = dependencies.get_usecase()
|
||||
await usecase.some_use_case(
|
||||
telegram_id=message.from_user.id,
|
||||
chat_id=message.chat.id
|
||||
)
|
||||
```
|
||||
|
||||
**Use Case Pattern (Telegram):**
|
||||
```python
|
||||
from src import domain
|
||||
|
||||
async def some_use_case(self: 'Usecase', telegram_id: int, chat_id: int) -> None:
|
||||
async with self.database.transaction():
|
||||
# 1. Получить/проверить пользователя
|
||||
user = await self.database.get_user(telegram_id=telegram_id)
|
||||
if not user:
|
||||
await self.telegram_bot.send_message('❌ Не авторизован', chat_id=chat_id)
|
||||
return
|
||||
|
||||
# 2. Получить состояние (если нужно)
|
||||
state = await self.database.get_telegram_state(telegram_id)
|
||||
|
||||
# 3. Бизнес-логика
|
||||
# ...
|
||||
|
||||
# 4. Обновить состояние
|
||||
await self.database.set_telegram_state(
|
||||
telegram_id=telegram_id,
|
||||
state=domain.TelegramStateEnum.SOME_STATE,
|
||||
context={'key': 'value'}
|
||||
)
|
||||
|
||||
# 5. Отправить сообщение
|
||||
await self.telegram_bot.send_message('✅ Успешно', chat_id=chat_id)
|
||||
```
|
||||
|
||||
**Inline Keyboard Pattern:**
|
||||
```python
|
||||
from aiogram.types import InlineKeyboardButton
|
||||
|
||||
# В use case:
|
||||
buttons = [
|
||||
[InlineKeyboardButton(text='Кнопка', callback_data='callback_id:value')]
|
||||
for item in items
|
||||
]
|
||||
await self.telegram_bot.send_message_with_inline_keyboard(
|
||||
'Выберите опцию:', chat_id=chat_id, buttons=buttons
|
||||
)
|
||||
```
|
||||
|
||||
**Callback Query Handler:**
|
||||
```python
|
||||
@telegram_callback_router.callback_query(F.data.startswith('prefix:'))
|
||||
async def callback_handler(callback: CallbackQuery) -> None:
|
||||
if not callback.from_user or not callback.data:
|
||||
return
|
||||
|
||||
usecase = dependencies.get_usecase()
|
||||
await usecase.handle_callback(
|
||||
telegram_id=callback.from_user.id,
|
||||
chat_id=callback.message.chat.id,
|
||||
callback_data=callback.data,
|
||||
message_id=callback.message.message_id
|
||||
)
|
||||
await callback.answer() # Убрать "loading" на кнопке
|
||||
```
|
||||
|
||||
**Text Message Filters:**
|
||||
```python
|
||||
# Обрабатывать только не-команды
|
||||
@telegram_callback_router.message(F.text & ~F.text.startswith('/'))
|
||||
async def text_handler(message: Message) -> None:
|
||||
# Этот handler НЕ сработает для команд типа /start
|
||||
...
|
||||
```
|
||||
|
||||
**Важные правила для Telegram handlers:**
|
||||
- Специфичные фильтры (Command) регистрировать РАНЬШЕ общих (F.text)
|
||||
- Использовать `~F.text.startswith('/')` для исключения команд в текстовых handlers
|
||||
- Всегда проверять `message.from_user` перед использованием
|
||||
- Callback handlers должны вызывать `await callback.answer()` в конце
|
||||
- НЕ импортировать aiogram типы в usecase layer (только через TYPE_CHECKING в Protocol)
|
||||
|
||||
### Adding New Protocol Methods
|
||||
|
||||
When adding new functionality that requires protocol methods:
|
||||
|
||||
**1. Define Protocol in usecase layer (`src/usecase/__init__.py`):**
|
||||
```python
|
||||
if typing.TYPE_CHECKING:
|
||||
from aiogram.types import InlineKeyboardButton # Импорт только для типов
|
||||
|
||||
class TelegramBotWriter(typing.Protocol):
|
||||
async def send_message_with_inline_keyboard(
|
||||
self, text: str, chat_id: int, buttons: list[list['InlineKeyboardButton']]
|
||||
) -> None: ...
|
||||
```
|
||||
|
||||
**2. Implement in adapter (`src/adapter/telegram_bot.py`):**
|
||||
```python
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
|
||||
class TelegramBot(TelegramBase):
|
||||
async def send_message_with_inline_keyboard(
|
||||
self, text: str, chat_id: int, buttons: list[list[InlineKeyboardButton]]
|
||||
) -> None:
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
await self.bot.send_message(chat_id=chat_id, text=text, reply_markup=keyboard)
|
||||
```
|
||||
|
||||
**3. Use in use case:**
|
||||
```python
|
||||
from aiogram.types import InlineKeyboardButton # Можно импортировать в use case
|
||||
|
||||
buttons = [[InlineKeyboardButton(text='Text', callback_data='data')]]
|
||||
await self.telegram_bot.send_message_with_inline_keyboard('Message', chat_id, buttons)
|
||||
```
|
||||
|
||||
**Типизация Enum в Domain:**
|
||||
- Используйте `enum.StrEnum` для строковых enum (Python 3.11+)
|
||||
- Enum значения в lowercase с underscores
|
||||
- Передавайте enum в SQLAlchemy через `Enum(EnumClass)`
|
||||
|
||||
```python
|
||||
import enum
|
||||
from sqlalchemy import Enum
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
class TelegramStateEnum(enum.StrEnum):
|
||||
CREATIVE_WAITING_CHANNEL = 'creative_waiting_channel'
|
||||
CREATIVE_WAITING_NAME = 'creative_waiting_name'
|
||||
|
||||
class TelegramState(Base):
|
||||
state: Mapped[TelegramStateEnum] = mapped_column(Enum(TelegramStateEnum), nullable=False)
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user