```markdown # Telegram Bot "Что почитать" - Техническое задание ## Описание проекта Telegram-бот для просмотра книг с описаниями, обложками и рейтингами. Бот является интеграцией к существующему веб-сервису на Django с общей PostgreSQL базой данных. ## Технологический стек - **Python 3.10+** - **aiogram 3.x** - асинхронный фреймворк для Telegram Bot API [web:1][web:3] - **asyncpg** или **psycopg2** - для работы с PostgreSQL [web:6] - **SQLAlchemy 2.0** (асинхронный режим) - ORM для работы с БД - **python-dotenv** - для хранения конфигурации ## Структура проекта ``` telegram_bot/ ├── .env \# Переменные окружения ├── .env.example \# Пример конфигурации ├── requirements.txt \# Зависимости ├── main.py \# Точка входа ├── config.py \# Конфигурация ├── database/ │ ├── __init__.py │ ├── connection.py \# Подключение к БД │ └── models.py \# SQLAlchemy модели ├── handlers/ │ ├── __init__.py │ ├── auth.py \# Авторизация/регистрация │ ├── books.py \# Просмотр книг │ └── favorites.py \# Избранное ├── keyboards/ │ ├── __init__.py │ └── inline.py \# Inline-клавиатуры[^1][^2] ├── middlewares/ │ ├── __init__.py │ └── auth.py \# Проверка авторизации └── utils/ ├── __init__.py └── states.py \# FSM состояния ``` ## Схема базы данных (уже существует) ### Таблица: users (из Django) - `id` - PRIMARY KEY - `username` - уникальное имя пользователя - `email` - уникальный email (используется для входа) - `password` - хешированный пароль - `telegram_id` - BigInteger, уникальный, nullable - `first_name`, `last_name` - имя и фамилия - остальные поля Django User ### Таблица: books_book - `id` - PRIMARY KEY - `title` - название книги (max 500) - `description` - описание (TEXT) - `cover_url` - ссылка на обложку (URL) - `average_rating` - средний рейтинг (Decimal 0.00-5.00) - `rating_count` - количество оценок - `language` - язык книги - `created_at`, `updated_at` - временные метки ### Таблица: books_author - `id` - PRIMARY KEY - `name` - имя автора ### Таблица: books_genre - `id` - PRIMARY KEY - `name` - название жанра ### Таблица: books_book_authors (ManyToMany) - `book_id` - FK на books_book - `author_id` - FK на books_author ### Таблица: books_book_genres (ManyToMany) - `book_id` - FK на books_book - `genre_id` - FK на books_genre ### Таблица: books_favorite - `id` - PRIMARY KEY - `user_id` - FK на users - `book_id` - FK на books_book - `created_at` - дата добавления - UNIQUE(user_id, book_id) ## Функциональные требования ### 1. Авторизация и регистрация **Команда:** `/start` **Логика:** - Проверить, есть ли у пользователя `telegram_id` в таблице `users` - Если да → авторизован, показать главное меню - Если нет → показать кнопки "Войти" и "Зарегистрироваться" **Регистрация:** 1. Кнопка "Зарегистрироваться" → запросить email 2. Проверить уникальность email в БД 3. Запросить username (проверить уникальность) 4. Запросить пароль (минимум 8 символов) 5. Создать запись в таблице `users` с `telegram_id = message.from_user.id` 6. Хешировать пароль через `bcrypt` или `django.contrib.auth.hashers` **Вход:** 1. Кнопка "Войти" → запросить email 2. Запросить пароль 3. Проверить credentials в БД 4. Если успешно → обновить `telegram_id` для этого пользователя 5. Показать главное меню ### 2. Главное меню (после авторизации) Inline-клавиатура [web:7]: - 📚 Все книги - ⭐ Избранное - 🔍 Поиск по жанру - 👤 Мой профиль ### 3. Просмотр всех книг **Запрос к БД:** ``` SELECT b.id, b.title, b.description, b.cover_url, b.average_rating, b.rating_count, STRING_AGG(DISTINCT a.name, ', ') as authors, STRING_AGG(DISTINCT g.name, ', ') as genres FROM books_book b LEFT JOIN books_book_authors ba ON b.id = ba.book_id LEFT JOIN books_author a ON ba.author_id = a.id LEFT JOIN books_book_genres bg ON b.id = bg.book_id LEFT JOIN books_genre g ON bg.genre_id = g.id WHERE b.deleted_at IS NULL GROUP BY b.id ORDER BY b.created_at DESC LIMIT 10 OFFSET ? ``` **Формат вывода:** ``` 📖 [Название книги] ✍️ Авторы: [список через запятую] 🏷 Жанры: [список через запятую] ⭐ Рейтинг: X.XX (Y оценок) [Описание книги, максимум 200 символов] [Картинка по cover_url] Кнопки: [❤️ В избранное] [➡️ След. страница] ``` **Пагинация:** по 10 книг на страницу, кнопки "⬅️ Назад" / "➡️ Вперед" ### 4. Избранное **Запрос к БД:** ``` SELECT b.id, b.title, b.cover_url, b.average_rating, STRING_AGG(DISTINCT a.name, ', ') as authors FROM books_favorite f JOIN books_book b ON f.book_id = b.id LEFT JOIN books_book_authors ba ON b.id = ba.book_id LEFT JOIN books_author a ON ba.author_id = a.id WHERE f.user_id = ? AND b.deleted_at IS NULL GROUP BY b.id, f.created_at ORDER BY f.created_at DESC ``` **Действия:** - Показать список избранных книг (формат как в п.3) - Кнопка "❌ Удалить из избранного" → DELETE FROM books_favorite ### 5. Добавление в избранное **Callback:** `add_fav:{book_id}` **Запрос:** ``` INSERT INTO books_favorite (user_id, book_id, created_at) VALUES (?, ?, NOW()) ON CONFLICT (user_id, book_id) DO NOTHING ``` **Ответ:** "✅ Книга добавлена в избранное!" ### 6. Поиск по жанру 1. Получить список жанров: `SELECT id, name FROM books_genre ORDER BY name` 2. Показать inline-клавиатуру с жанрами (по 2 в строке) [web:7] 3. После выбора жанра → фильтровать книги: ``` SELECT DISTINCT b.* FROM books_book b JOIN books_book_genres bg ON b.id = bg.book_id WHERE bg.genre_id = ? AND b.deleted_at IS NULL ``` ## Технические требования ### 1. Подключение к базе данных **Файл:** `database/connection.py` ``` from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from config import DATABASE_URL engine = create_async_engine(DATABASE_URL, echo=False) async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) async def get_session(): async with async_session() as session: yield session ``` **Конфигурация .env:** ``` BOT_TOKEN=your_bot_token_here DATABASE_URL=postgresql+asyncpg://user:password@host:port/dbname ``` ### 2. Middleware для проверки авторизации [web:3] **Файл:** `middlewares/auth.py` ``` from aiogram import BaseMiddleware from aiogram.types import Message, CallbackQuery class AuthMiddleware(BaseMiddleware): async def __call__(self, handler, event, data): \# Пропускать команду /start if isinstance(event, Message) and event.text == '/start': return await handler(event, data) # Проверить telegram_id в БД user = await get_user_by_telegram_id(event.from_user.id) if not user: await event.answer("⛔ Сначала авторизуйтесь через /start") return data['user'] = user return await handler(event, data) ``` ### 3. FSM для авторизации **Файл:** `utils/states.py` ``` from aiogram.fsm.state import State, StatesGroup class RegistrationStates(StatesGroup): waiting_email = State() waiting_username = State() waiting_password = State() class LoginStates(StatesGroup): waiting_email = State() waiting_password = State() ``` ### 4. Inline-клавиатуры [web:7][web:10] **Файл:** `keyboards/inline.py` ``` from aiogram.utils.keyboard import InlineKeyboardBuilder from aiogram.types import InlineKeyboardButton def get_main_menu(): builder = InlineKeyboardBuilder() builder.button(text="📚 Все книги", callback_data="books_all") builder.button(text="⭐ Избранное", callback_data="favorites") builder.button(text="🔍 Поиск по жанру", callback_data="search_genre") builder.button(text="👤 Мой профиль", callback_data="profile") builder.adjust(2, 2) \# 2 кнопки в ряду return builder.as_markup() ``` ### 5. Обработка картинок Использовать `bot.send_photo()` с параметром `photo=cover_url` (если URL публичный). Если картинка не загружается → показать текстовую заглушку: "🖼 [Обложка недоступна]" ## Требования к коду 1. **Асинхронность:** все функции БД и хендлеры должны быть `async/await` [web:3] 2. **Обработка ошибок:** try/except для всех запросов к БД 3. **Логирование:** использовать `logging` для отладки 4. **Типизация:** использовать type hints везде 5. **Документация:** docstrings для всех функций 6. **Безопасность:** - Хешировать пароли через `bcrypt` - Не логировать пароли - SQL-инъекции предотвращаются через SQLAlchemy 7. **Простота:** избегать избыточных абстракций, код должен быть понятным ## Примеры запросов SQLAlchemy **Получить пользователя:** ``` from sqlalchemy import select from database.models import User async def get_user_by_telegram_id(telegram_id: int): async with async_session() as session: result = await session.execute( select(User).where(User.telegram_id == telegram_id) ) return result.scalar_one_or_none() ``` **Получить книги с авторами (raw SQL через SQLAlchemy):** ``` from sqlalchemy import text async def get_books_with_details(offset: int = 0, limit: int = 10): async with async_session() as session: query = text(""" SELECT b.id, b.title, b.description, b.cover_url, b.average_rating, b.rating_count, STRING_AGG(DISTINCT a.name, ', ') as authors, STRING_AGG(DISTINCT g.name, ', ') as genres FROM books_book b LEFT JOIN books_book_authors ba ON b.id = ba.book_id LEFT JOIN books_author a ON ba.author_id = a.id LEFT JOIN books_book_genres bg ON b.id = bg.book_id LEFT JOIN books_genre g ON bg.genre_id = g.id WHERE b.deleted_at IS NULL GROUP BY b.id ORDER BY b.created_at DESC LIMIT :limit OFFSET :offset """) result = await session.execute(query, {"limit": limit, "offset": offset}) return result.mappings().all() ``` ## Зависимости (requirements.txt) ``` aiogram>=3.4.0 asyncpg>=0.29.0 SQLAlchemy>=2.0.0 python-dotenv>=1.0.0 bcrypt>=4.1.0 ``` ## Дополнительные указания 1. **Начните с файла `main.py`** - инициализация бота, диспетчера, подключение роутеров [web:8] 2. **Используйте роутеры aiogram** для группировки хендлеров по функциональности [web:2] 3. **Тестируйте каждую функцию по отдельности** перед интеграцией 4. **Используйте callback_data формата:** `action:param` (например, `add_fav:123`) 5. **Обрабатывайте длинные сообщения:** если описание книги > 1024 символов, обрезать с "..." 6. **Escape специальных символов** в Markdown, если используете `parse_mode='Markdown'` ## Важно - НЕ модифицировать существующую Django БД - НЕ создавать новые таблицы - Использовать только SELECT, INSERT, UPDATE, DELETE для существующих таблиц - Следовать naming conventions Django (таблицы `app_model`, например `books_book`) - При обновлении `telegram_id` проверять уникальность (один Telegram аккаунт = один пользователь) ## Пример main.py ``` import asyncio import logging from aiogram import Bot, Dispatcher from aiogram.fsm.storage.memory import MemoryStorage from config import BOT_TOKEN from handlers import auth, books, favorites from middlewares.auth import AuthMiddleware logging.basicConfig(level=logging.INFO) async def main(): bot = Bot(token=BOT_TOKEN) dp = Dispatcher(storage=MemoryStorage()) # Регистрация middleware dp.message.middleware(AuthMiddleware()) dp.callback_query.middleware(AuthMiddleware()) # Регистрация роутеров dp.include_router(auth.router) dp.include_router(books.router) dp.include_router(favorites.router) await bot.delete_webhook(drop_pending_updates=True) await dp.start_polling(bot) if __name__ == '__main__': asyncio.run(main()) ``` Начни реализацию с базовой структуры проекта и авторизации! ``` Этот MD-файл содержит полную спецификацию для Cursor. Он включает:[^3][^4][^5] - Четкую структуру проекта с организацией файлов[^6] - Схему существующей Django БД (на основе ваших моделей) - Детальные функциональные требования для каждой фичи - Примеры SQL-запросов и SQLAlchemy кода[^7] - Инструкции по работе с inline-клавиатурами aiogram[^2][^1] - Требования к асинхронности и middleware[^5] - Готовый пример точки входа приложения Просто скопируйте этот контент в `.cursorrules` или отдельный MD-файл в корне проекта, и Cursor сможет генерировать код согласно спецификации!