initial commit

This commit is contained in:
2025-12-25 15:25:46 +03:00
commit 58827ac124
20 changed files with 1854 additions and 0 deletions

5
handlers/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
"""Модуль обработчиков событий бота"""
from handlers import auth, books, favorites
__all__ = ['auth', 'books', 'favorites']

340
handlers/auth.py Normal file
View File

@@ -0,0 +1,340 @@
from aiogram import Router, F
from aiogram.filters import CommandStart
from aiogram.types import Message, CallbackQuery
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from database.models import User
from database.connection import async_session_maker
from utils.states import RegistrationStates, LoginStates
from keyboards.inline import get_auth_menu, get_main_menu, get_back_to_menu_keyboard
import bcrypt
import logging
import re
router = Router()
logger = logging.getLogger(__name__)
@router.message(CommandStart())
async def cmd_start(message: Message, state: FSMContext):
"""Обработчик команды /start"""
await state.clear()
telegram_id = message.from_user.id
try:
async with async_session_maker() as session:
result = await session.execute(
select(User).where(User.telegram_id == telegram_id)
)
user = result.scalar_one_or_none()
if user:
# Пользователь авторизован
await message.answer(
f"👋 Добро пожаловать, {user.first_name or user.username}!\n\n"
"📚 Выберите действие:",
reply_markup=get_main_menu()
)
else:
# Пользователь не авторизован
await message.answer(
"👋 Добро пожаловать в бот 'Что почитать'!\n\n"
"📚 Здесь вы можете просматривать книги с описаниями, обложками и рейтингами.\n\n"
"Пожалуйста, войдите или зарегистрируйтесь:",
reply_markup=get_auth_menu()
)
except Exception as e:
logger.error(f"Ошибка в cmd_start: {e}")
await message.answer("❌ Произошла ошибка. Попробуйте позже.")
@router.callback_query(F.data == "main_menu")
async def show_main_menu(callback: CallbackQuery, state: FSMContext):
"""Показать главное меню"""
await state.clear()
telegram_id = callback.from_user.id
try:
async with async_session_maker() as session:
result = await session.execute(
select(User).where(User.telegram_id == telegram_id)
)
user = result.scalar_one_or_none()
if user:
await callback.message.edit_text(
f"👋 Добро пожаловать, {user.first_name or user.username}!\n\n"
"📚 Выберите действие:",
reply_markup=get_main_menu()
)
else:
await callback.message.edit_text(
"👋 Добро пожаловать в бот 'Что почитать'!\n\n"
"Пожалуйста, войдите или зарегистрируйтесь:",
reply_markup=get_auth_menu()
)
await callback.answer()
except Exception as e:
logger.error(f"Ошибка в show_main_menu: {e}")
await callback.answer("❌ Ошибка", show_alert=True)
# === РЕГИСТРАЦИЯ ===
@router.callback_query(F.data == "register")
async def start_registration(callback: CallbackQuery, state: FSMContext):
"""Начало регистрации"""
await state.set_state(RegistrationStates.waiting_email)
await callback.message.edit_text(
"📝 Регистрация\n\n"
"Введите ваш email:"
)
await callback.answer()
@router.message(RegistrationStates.waiting_email)
async def process_registration_email(message: Message, state: FSMContext):
"""Обработка email при регистрации"""
email = message.text.strip().lower()
# Валидация email
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(email_pattern, email):
await message.answer("❌ Неверный формат email. Попробуйте снова:")
return
try:
async with async_session_maker() as session:
# Проверка уникальности email
result = await session.execute(
select(User).where(User.email == email)
)
existing_user = result.scalar_one_or_none()
if existing_user:
await message.answer(
"❌ Этот email уже зарегистрирован.\n\n"
"Используйте другой email или войдите в систему."
)
await state.clear()
return
await state.update_data(email=email)
await state.set_state(RegistrationStates.waiting_username)
await message.answer("✅ Email принят.\n\nТеперь введите желаемое имя пользователя (username):")
except Exception as e:
logger.error(f"Ошибка при проверке email: {e}")
await message.answer("❌ Произошла ошибка. Попробуйте снова.")
await state.clear()
@router.message(RegistrationStates.waiting_username)
async def process_registration_username(message: Message, state: FSMContext):
"""Обработка username при регистрации"""
username = message.text.strip()
# Валидация username
if len(username) < 3 or len(username) > 150:
await message.answer("❌ Имя пользователя должно быть от 3 до 150 символов. Попробуйте снова:")
return
if not re.match(r'^[a-zA-Z0-9_]+$', username):
await message.answer("❌ Имя пользователя может содержать только буквы, цифры и подчеркивание. Попробуйте снова:")
return
try:
async with async_session_maker() as session:
# Проверка уникальности username
result = await session.execute(
select(User).where(User.username == username)
)
existing_user = result.scalar_one_or_none()
if existing_user:
await message.answer("❌ Это имя пользователя уже занято. Попробуйте другое:")
return
await state.update_data(username=username)
await state.set_state(RegistrationStates.waiting_password)
await message.answer(
"✅ Имя пользователя принято.\n\n"
"Теперь введите пароль (минимум 8 символов):"
)
except Exception as e:
logger.error(f"Ошибка при проверке username: {e}")
await message.answer("❌ Произошла ошибка. Попробуйте снова.")
await state.clear()
@router.message(RegistrationStates.waiting_password)
async def process_registration_password(message: Message, state: FSMContext):
"""Обработка пароля и завершение регистрации"""
password = message.text.strip()
# Валидация пароля
if len(password) < 8:
await message.answer("❌ Пароль должен содержать минимум 8 символов. Попробуйте снова:")
return
try:
# Удаляем сообщение с паролем из чата
await message.delete()
except:
pass
try:
data = await state.get_data()
email = data['email']
username = data['username']
# Хешируем пароль
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
async with async_session_maker() as session:
# Создаем нового пользователя
new_user = User(
email=email,
username=username,
password=hashed_password,
telegram_id=message.from_user.id,
first_name=message.from_user.first_name or "",
last_name=message.from_user.last_name or "",
is_active=True
)
session.add(new_user)
await session.commit()
await message.answer(
f"✅ Регистрация успешно завершена!\n\n"
f"👤 Ваш username: {username}\n"
f"📧 Email: {email}\n\n"
"Добро пожаловать в бот 'Что почитать'!",
reply_markup=get_main_menu()
)
await state.clear()
except Exception as e:
logger.error(f"Ошибка при создании пользователя: {e}")
await message.answer("❌ Произошла ошибка при регистрации. Попробуйте снова.")
await state.clear()
# === ВХОД ===
@router.callback_query(F.data == "login")
async def start_login(callback: CallbackQuery, state: FSMContext):
"""Начало входа"""
await state.set_state(LoginStates.waiting_email)
await callback.message.edit_text(
"🔐 Вход в систему\n\n"
"Введите ваш email:"
)
await callback.answer()
@router.message(LoginStates.waiting_email)
async def process_login_email(message: Message, state: FSMContext):
"""Обработка email при входе"""
email = message.text.strip().lower()
await state.update_data(email=email)
await state.set_state(LoginStates.waiting_password)
await message.answer("Теперь введите ваш пароль:")
@router.message(LoginStates.waiting_password)
async def process_login_password(message: Message, state: FSMContext):
"""Обработка пароля и завершение входа"""
password = message.text.strip()
try:
# Удаляем сообщение с паролем
await message.delete()
except:
pass
try:
data = await state.get_data()
email = data['email']
async with async_session_maker() as session:
result = await session.execute(
select(User).where(User.email == email)
)
user = result.scalar_one_or_none()
if not user:
await message.answer("❌ Пользователь с таким email не найден.")
await state.clear()
return
# Проверяем пароль
if not bcrypt.checkpw(password.encode('utf-8'), user.password.encode('utf-8')):
await message.answer("❌ Неверный пароль.")
await state.clear()
return
# Проверяем, не привязан ли уже другой Telegram аккаунт
if user.telegram_id and user.telegram_id != message.from_user.id:
await message.answer("❌ Этот аккаунт уже привязан к другому Telegram профилю.")
await state.clear()
return
# Обновляем telegram_id
user.telegram_id = message.from_user.id
await session.commit()
await message.answer(
f"✅ Вход выполнен успешно!\n\n"
f"👋 Добро пожаловать, {user.first_name or user.username}!",
reply_markup=get_main_menu()
)
await state.clear()
except Exception as e:
logger.error(f"Ошибка при входе: {e}")
await message.answer("❌ Произошла ошибка при входе. Попробуйте снова.")
await state.clear()
# === ПРОФИЛЬ ===
@router.callback_query(F.data == "profile")
async def show_profile(callback: CallbackQuery):
"""Показать профиль пользователя"""
telegram_id = callback.from_user.id
try:
async with async_session_maker() as session:
result = await session.execute(
select(User).where(User.telegram_id == telegram_id)
)
user = result.scalar_one_or_none()
if not user:
await callback.answer("⛔ Пользователь не найден", show_alert=True)
return
profile_text = (
f"👤 Ваш профиль\n\n"
f"🆔 Username: {user.username}\n"
f"📧 Email: {user.email}\n"
f"👤 Имя: {user.first_name or 'Не указано'}\n"
f"📅 Дата регистрации: {user.date_joined.strftime('%d.%m.%Y')}"
)
await callback.message.edit_text(
profile_text,
reply_markup=get_back_to_menu_keyboard()
)
await callback.answer()
except Exception as e:
logger.error(f"Ошибка в show_profile: {e}")
await callback.answer("❌ Ошибка", show_alert=True)

306
handlers/books.py Normal file
View File

@@ -0,0 +1,306 @@
from aiogram import Router, F
from aiogram.types import CallbackQuery, InputMediaPhoto
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, text, func
from database.models import Book, Author, Genre, Favorite, User
from database.connection import async_session_maker
from keyboards.inline import get_book_keyboard, get_pagination_keyboard, get_genres_keyboard, get_main_menu
import logging
router = Router()
logger = logging.getLogger(__name__)
BOOKS_PER_PAGE = 10
async def get_books_with_details(session: AsyncSession, offset: int = 0, limit: int = 10, genre_id: int = None):
"""Получить книги с авторами и жанрами"""
try:
if genre_id:
query = text("""
SELECT DISTINCT
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
AND b.id IN (
SELECT book_id FROM books_book_genres WHERE genre_id = :genre_id
)
GROUP BY b.id
ORDER BY b.created_at DESC
LIMIT :limit OFFSET :offset
""")
result = await session.execute(query, {"limit": limit, "offset": offset, "genre_id": genre_id})
else:
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()
except Exception as e:
logger.error(f"Ошибка при получении книг: {e}")
return []
async def get_total_books_count(session: AsyncSession, genre_id: int = None) -> int:
"""Получить общее количество книг"""
try:
if genre_id:
query = text("""
SELECT COUNT(DISTINCT b.id)
FROM books_book b
JOIN books_book_genres bg ON b.id = bg.book_id
WHERE b.deleted_at IS NULL AND bg.genre_id = :genre_id
""")
result = await session.execute(query, {"genre_id": genre_id})
else:
query = text("""
SELECT COUNT(*)
FROM books_book
WHERE deleted_at IS NULL
""")
result = await session.execute(query)
return result.scalar() or 0
except Exception as e:
logger.error(f"Ошибка при подсчете книг: {e}")
return 0
async def is_book_favorite(session: AsyncSession, user_id: int, book_id: int) -> bool:
"""Проверить, находится ли книга в избранном"""
try:
result = await session.execute(
select(Favorite).where(
Favorite.user_id == user_id,
Favorite.book_id == book_id
)
)
return result.scalar_one_or_none() is not None
except Exception as e:
logger.error(f"Ошибка при проверке избранного: {e}")
return False
def format_book_message(book: dict, is_favorite: bool = False) -> str:
"""Форматировать сообщение о книге"""
title = book['title']
authors = book['authors'] or 'Не указано'
genres = book['genres'] or 'Не указано'
rating = float(book['average_rating']) if book['average_rating'] else 0.0
rating_count = book['rating_count'] or 0
description = book['description'] or 'Описание отсутствует'
# Обрезаем описание если оно слишком длинное
if len(description) > 500:
description = description[:497] + "..."
# Формируем звездочки для рейтинга
stars = "" * int(rating)
message = (
f"📖 <b>{title}</b>\n\n"
f"✍️ <b>Авторы:</b> {authors}\n"
f"🏷 <b>Жанры:</b> {genres}\n"
f"{stars} <b>Рейтинг:</b> {rating:.2f} ({rating_count} оценок)\n\n"
f"{description}"
)
if is_favorite:
message = "❤️ " + message
return message
@router.callback_query(F.data == "books_all")
async def show_all_books(callback: CallbackQuery, state: FSMContext):
"""Показать все книги (первая страница)"""
await state.clear()
await show_books_page(callback, 0)
@router.callback_query(F.data.startswith("books_page:"))
async def handle_books_pagination(callback: CallbackQuery):
"""Обработка пагинации книг"""
page = int(callback.data.split(":")[1])
await show_books_page(callback, page)
async def show_books_page(callback: CallbackQuery, page: int, genre_id: int = None):
"""Показать страницу с книгами"""
telegram_id = callback.from_user.id
try:
async with async_session_maker() as session:
# Получаем пользователя
result = await session.execute(
select(User).where(User.telegram_id == telegram_id)
)
user = result.scalar_one_or_none()
if not user:
await callback.answer("⛔ Пользователь не найден", show_alert=True)
return
# Получаем общее количество книг
total_books = await get_total_books_count(session, genre_id)
if total_books == 0:
await callback.message.edit_text(
"📚 Книги не найдены",
reply_markup=get_main_menu()
)
await callback.answer()
return
total_pages = (total_books + BOOKS_PER_PAGE - 1) // BOOKS_PER_PAGE
# Проверяем корректность страницы
if page < 0 or page >= total_pages:
await callback.answer("❌ Страница не найдена", show_alert=True)
return
# Получаем книги для страницы
books = await get_books_with_details(
session,
offset=page * BOOKS_PER_PAGE,
limit=BOOKS_PER_PAGE,
genre_id=genre_id
)
if not books:
await callback.answer("❌ Книги не найдены", show_alert=True)
return
# Показываем первую книгу на странице
book = books[0]
is_favorite = await is_book_favorite(session, user.id, book['id'])
message_text = format_book_message(book, is_favorite)
keyboard = get_book_keyboard(book['id'], is_favorite, page, total_pages)
# Пытаемся отправить с картинкой
if book['cover_url']:
try:
if callback.message.photo:
await callback.message.edit_media(
media=InputMediaPhoto(
media=book['cover_url'],
caption=message_text,
parse_mode="HTML"
),
reply_markup=keyboard
)
else:
await callback.message.delete()
await callback.message.answer_photo(
photo=book['cover_url'],
caption=message_text,
parse_mode="HTML",
reply_markup=keyboard
)
except Exception as e:
logger.warning(f"Не удалось загрузить обложку: {e}")
await callback.message.edit_text(
"🖼 [Обложка недоступна]\n\n" + message_text,
parse_mode="HTML",
reply_markup=keyboard
)
else:
await callback.message.edit_text(
message_text,
parse_mode="HTML",
reply_markup=keyboard
)
await callback.answer()
except Exception as e:
logger.error(f"Ошибка в show_books_page: {e}")
await callback.answer("❌ Произошла ошибка", show_alert=True)
# === ПОИСК ПО ЖАНРУ ===
@router.callback_query(F.data == "search_genre")
async def show_genres(callback: CallbackQuery):
"""Показать список жанров"""
try:
async with async_session_maker() as session:
result = await session.execute(
select(Genre.id, Genre.name).order_by(Genre.name)
)
genres = result.all()
if not genres:
await callback.message.edit_text(
"📚 Жанры не найдены",
reply_markup=get_main_menu()
)
await callback.answer()
return
await callback.message.edit_text(
"🔍 Выберите жанр для поиска книг:",
reply_markup=get_genres_keyboard(genres)
)
await callback.answer()
except Exception as e:
logger.error(f"Ошибка в show_genres: {e}")
await callback.answer("❌ Произошла ошибка", show_alert=True)
@router.callback_query(F.data.startswith("genre:"))
async def show_books_by_genre(callback: CallbackQuery, state: FSMContext):
"""Показать книги выбранного жанра"""
genre_id = int(callback.data.split(":")[1])
try:
async with async_session_maker() as session:
# Получаем название жанра
result = await session.execute(
select(Genre).where(Genre.id == genre_id)
)
genre = result.scalar_one_or_none()
if not genre:
await callback.answer("❌ Жанр не найден", show_alert=True)
return
# Сохраняем genre_id в состоянии для пагинации
await state.update_data(genre_id=genre_id)
await show_books_page(callback, 0, genre_id)
except Exception as e:
logger.error(f"Ошибка в show_books_by_genre: {e}")
await callback.answer("❌ Произошла ошибка", show_alert=True)
@router.callback_query(F.data == "page_info")
async def page_info_handler(callback: CallbackQuery):
"""Обработчик для информационной кнопки страницы"""
await callback.answer(" Текущая страница")

252
handlers/favorites.py Normal file
View File

@@ -0,0 +1,252 @@
from aiogram import Router, F
from aiogram.types import CallbackQuery, InputMediaPhoto
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, text, delete
from database.models import Book, Favorite, User
from database.connection import async_session_maker
from keyboards.inline import get_book_keyboard, get_main_menu
from handlers.books import format_book_message, is_book_favorite
import logging
router = Router()
logger = logging.getLogger(__name__)
@router.callback_query(F.data.startswith("add_fav:"))
async def add_to_favorites(callback: CallbackQuery):
"""Добавить книгу в избранное"""
book_id = int(callback.data.split(":")[1])
telegram_id = callback.from_user.id
try:
async with async_session_maker() as session:
# Получаем пользователя
result = await session.execute(
select(User).where(User.telegram_id == telegram_id)
)
user = result.scalar_one_or_none()
if not user:
await callback.answer("⛔ Пользователь не найден", show_alert=True)
return
# Проверяем, существует ли книга
result = await session.execute(
select(Book).where(Book.id == book_id, Book.deleted_at.is_(None))
)
book = result.scalar_one_or_none()
if not book:
await callback.answer("❌ Книга не найдена", show_alert=True)
return
# Проверяем, не добавлена ли уже
result = await session.execute(
select(Favorite).where(
Favorite.user_id == user.id,
Favorite.book_id == book_id
)
)
existing_favorite = result.scalar_one_or_none()
if existing_favorite:
await callback.answer(" Книга уже в избранном", show_alert=True)
return
# Добавляем в избранное
new_favorite = Favorite(
user_id=user.id,
book_id=book_id
)
session.add(new_favorite)
await session.commit()
# Обновляем клавиатуру
try:
current_markup = callback.message.reply_markup
# Извлекаем номер страницы из существующей клавиатуры
page = 0
total_pages = 1
if current_markup and current_markup.inline_keyboard:
for row in current_markup.inline_keyboard:
for button in row:
if button.callback_data and "books_page:" in button.callback_data:
parts = button.callback_data.split(":")
if len(parts) > 1:
page = int(parts[1])
elif button.text and "/" in button.text:
try:
pages_info = button.text.split("/")
page = int(pages_info[0]) - 1
total_pages = int(pages_info[1])
except:
pass
new_keyboard = get_book_keyboard(book_id, True, page, total_pages)
await callback.message.edit_reply_markup(reply_markup=new_keyboard)
except Exception as e:
logger.warning(f"Не удалось обновить клавиатуру: {e}")
await callback.answer("✅ Книга добавлена в избранное!")
except Exception as e:
logger.error(f"Ошибка при добавлении в избранное: {e}")
await callback.answer("❌ Произошла ошибка", show_alert=True)
@router.callback_query(F.data.startswith("remove_fav:"))
async def remove_from_favorites(callback: CallbackQuery):
"""Удалить книгу из избранного"""
book_id = int(callback.data.split(":")[1])
telegram_id = callback.from_user.id
try:
async with async_session_maker() as session:
# Получаем пользователя
result = await session.execute(
select(User).where(User.telegram_id == telegram_id)
)
user = result.scalar_one_or_none()
if not user:
await callback.answer("⛔ Пользователь не найден", show_alert=True)
return
# Удаляем из избранного
await session.execute(
delete(Favorite).where(
Favorite.user_id == user.id,
Favorite.book_id == book_id
)
)
await session.commit()
# Обновляем клавиатуру
try:
current_markup = callback.message.reply_markup
page = 0
total_pages = 1
if current_markup and current_markup.inline_keyboard:
for row in current_markup.inline_keyboard:
for button in row:
if button.callback_data and "books_page:" in button.callback_data:
parts = button.callback_data.split(":")
if len(parts) > 1:
page = int(parts[1])
elif button.text and "/" in button.text:
try:
pages_info = button.text.split("/")
page = int(pages_info[0]) - 1
total_pages = int(pages_info[1])
except:
pass
new_keyboard = get_book_keyboard(book_id, False, page, total_pages)
await callback.message.edit_reply_markup(reply_markup=new_keyboard)
except Exception as e:
logger.warning(f"Не удалось обновить клавиатуру: {e}")
await callback.answer("❌ Книга удалена из избранного")
except Exception as e:
logger.error(f"Ошибка при удалении из избранного: {e}")
await callback.answer("❌ Произошла ошибка", show_alert=True)
@router.callback_query(F.data == "favorites")
async def show_favorites(callback: CallbackQuery):
"""Показать избранные книги"""
telegram_id = callback.from_user.id
try:
async with async_session_maker() as session:
# Получаем пользователя
result = await session.execute(
select(User).where(User.telegram_id == telegram_id)
)
user = result.scalar_one_or_none()
if not user:
await callback.answer("⛔ Пользователь не найден", show_alert=True)
return
# Получаем избранные книги с полной информацией
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_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
LEFT JOIN books_book_genres bg ON b.id = bg.book_id
LEFT JOIN books_genre g ON bg.genre_id = g.id
WHERE f.user_id = :user_id AND b.deleted_at IS NULL
GROUP BY b.id, f.created_at
ORDER BY f.created_at DESC
LIMIT 1
""")
result = await session.execute(query, {"user_id": user.id})
favorites = result.mappings().all()
if not favorites:
await callback.message.edit_text(
"У вас пока нет избранных книг.\n\n"
"Добавьте книги в избранное, чтобы быстро находить их здесь!",
reply_markup=get_main_menu()
)
await callback.answer()
return
# Показываем первую избранную книгу
book = favorites[0]
message_text = format_book_message(book, is_favorite=True)
# Создаем клавиатуру (всегда с кнопкой удаления из избранного)
keyboard = get_book_keyboard(book['id'], True, 0, 1)
# Отправляем с картинкой если есть
if book['cover_url']:
try:
if callback.message.photo:
await callback.message.edit_media(
media=InputMediaPhoto(
media=book['cover_url'],
caption=message_text,
parse_mode="HTML"
),
reply_markup=keyboard
)
else:
await callback.message.delete()
await callback.message.answer_photo(
photo=book['cover_url'],
caption=message_text,
parse_mode="HTML",
reply_markup=keyboard
)
except Exception as e:
logger.warning(f"Не удалось загрузить обложку: {e}")
await callback.message.edit_text(
"🖼 [Обложка недоступна]\n\n" + message_text,
parse_mode="HTML",
reply_markup=keyboard
)
else:
await callback.message.edit_text(
message_text,
parse_mode="HTML",
reply_markup=keyboard
)
await callback.answer()
except Exception as e:
logger.error(f"Ошибка в show_favorites: {e}")
await callback.answer("❌ Произошла ошибка", show_alert=True)