from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from database.db import async_session_maker
from database.models import WordSource
from services.user_service import UserService
from services.vocabulary_service import VocabularyService
from services.ai_service import ai_service
from utils.i18n import t, get_user_lang
router = Router()
class AddWordStates(StatesGroup):
"""Состояния для добавления слова"""
waiting_for_confirmation = State()
waiting_for_word = State()
@router.message(Command("add"))
async def cmd_add(message: Message, state: FSMContext):
"""Обработчик команды /add [слово]"""
# Получаем слово из команды
parts = message.text.split(maxsplit=1)
if len(parts) < 2:
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
await message.answer(t(lang, 'add.prompt'))
await state.set_state(AddWordStates.waiting_for_word)
return
word = parts[1].strip()
await process_word_addition(message, state, word)
@router.message(AddWordStates.waiting_for_word)
async def process_word_input(message: Message, state: FSMContext):
"""Обработка ввода слова"""
word = message.text.strip()
await process_word_addition(message, state, word)
async def process_word_addition(message: Message, state: FSMContext, word: str):
"""Обработка добавления слова"""
# Получаем пользователя
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
if not user:
await message.answer(t('ru', 'common.start_first'))
return
# Проверяем, есть ли уже такое слово
existing_word = await VocabularyService.find_word(session, user.id, word)
if existing_word:
lang = get_user_lang(user)
await message.answer(t(lang, 'add.exists', word=word, translation=existing_word.word_translation))
await state.clear()
return
# Показываем индикатор загрузки
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
processing_msg = await message.answer(t(lang, 'add.searching'))
# Получаем перевод через AI
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
source_lang = user.learning_language if user else 'en'
ui_lang = user.language_interface if user else 'ru'
word_data = await ai_service.translate_word(word, source_lang=source_lang, translation_lang=ui_lang)
# Удаляем сообщение о загрузке
await processing_msg.delete()
# Формируем примеры
examples_text = ""
if word_data.get("examples"):
examples_text = "\n\n" + t(lang, 'add.examples_header') + "\n"
for idx, example in enumerate(word_data["examples"][:2], 1):
src = example.get(source_lang) or example.get('en') or example.get('ru') or ''
tr = example.get(ui_lang) or example.get('ru') or example.get('en') or ''
examples_text += f"{idx}. {src}\n {tr}\n"
# Отправляем карточку слова
card_text = (
f"📝 {word_data['word']}\n"
f"🔊 [{word_data.get('transcription', '')}]\n\n"
f"{t(lang, 'add.translation_label')}: {word_data['translation']}\n"
f"{t(lang, 'add.category_label')}: {word_data.get('category', '')}\n"
f"{t(lang, 'add.level_label')}: {word_data.get('difficulty', 'A1')}"
f"{examples_text}\n\n"
f"{t(lang, 'add.confirm_question')}"
)
# Создаём inline-кнопки
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text=t(lang, 'add.btn_add'), callback_data=f"add_word_confirm"),
InlineKeyboardButton(text=t(lang, 'add.btn_cancel'), callback_data="add_word_cancel")
]
])
# Сохраняем данные слова в состоянии
await state.update_data(word_data=word_data, user_id=user.id)
await state.set_state(AddWordStates.waiting_for_confirmation)
await message.answer(card_text, reply_markup=keyboard)
@router.callback_query(F.data == "add_word_confirm", AddWordStates.waiting_for_confirmation)
async def confirm_add_word(callback: CallbackQuery, state: FSMContext):
"""Подтверждение добавления слова"""
# Отвечаем сразу, запись в БД и подсчёт могут занять время
await callback.answer()
data = await state.get_data()
word_data = data.get("word_data")
user_id = data.get("user_id")
async with async_session_maker() as session:
# Получаем пользователя для языков
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
source_lang = user.learning_language if user else 'en'
ui_lang = user.language_interface if user else 'ru'
# Добавляем слово в базу
await VocabularyService.add_word(
session,
user_id=user_id,
word_original=word_data["word"],
word_translation=word_data["translation"],
source_lang=source_lang,
translation_lang=ui_lang,
transcription=word_data.get("transcription"),
examples={"examples": word_data.get("examples", [])},
category=word_data.get("category"),
difficulty_level=word_data.get("difficulty"),
source=WordSource.MANUAL
)
# Получаем общее количество слов
words_count = await VocabularyService.get_words_count(session, user_id, learning_lang=user.learning_language)
lang = ui_lang or 'ru'
await callback.message.edit_text(
t(lang, 'add.added_success', word=word_data['word'], count=words_count)
)
await state.clear()
@router.callback_query(F.data == "add_word_cancel")
async def cancel_add_word(callback: CallbackQuery, state: FSMContext):
"""Отмена добавления слова"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = (user.language_interface if user else 'ru') or 'ru'
await callback.message.edit_text(t(lang, 'add.cancelled'))
await state.clear()
await callback.answer()
WORDS_PER_PAGE = 10
@router.message(Command("vocabulary"))
async def cmd_vocabulary(message: Message):
"""Обработчик команды /vocabulary"""
await show_vocabulary_page(message, page=0)
async def show_vocabulary_page(message_or_callback, page: int = 0, edit: bool = False):
"""Показать страницу словаря"""
# Определяем, это Message или CallbackQuery
# В CallbackQuery from_user — это пользователь, а message.from_user — бот
user_id = message_or_callback.from_user.id
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, user_id)
if not user:
if edit:
await message_or_callback.message.edit_text(t('ru', 'common.start_first'))
else:
await message_or_callback.answer(t('ru', 'common.start_first'))
return
# Получаем слова с пагинацией
offset = page * WORDS_PER_PAGE
words = await VocabularyService.get_user_words(
session, user.id,
limit=WORDS_PER_PAGE,
offset=offset,
learning_lang=user.learning_language
)
total_count = await VocabularyService.get_words_count(session, user.id, learning_lang=user.learning_language)
lang = get_user_lang(user)
if not words and page == 0:
if edit:
await message_or_callback.message.edit_text(t(lang, 'vocab.empty'))
else:
await message_or_callback.answer(t(lang, 'vocab.empty'))
return
# Формируем список слов
total_pages = (total_count + WORDS_PER_PAGE - 1) // WORDS_PER_PAGE
words_list = t(lang, 'vocab.header') + "\n\n"
for idx, word in enumerate(words, start=offset + 1):
progress = ""
if word.times_reviewed > 0:
accuracy = int((word.correct_answers / word.times_reviewed) * 100)
progress = " " + t(lang, 'vocab.accuracy_inline', n=accuracy)
words_list += (
f"{idx}. {word.word_original} — {word.word_translation}\n"
f" 🔊 [{word.transcription or ''}]{progress}\n\n"
)
words_list += t(lang, 'vocab.page_info', page=page + 1, total=total_pages, count=total_count)
# Кнопки пагинации
buttons = []
nav_row = []
if page > 0:
nav_row.append(InlineKeyboardButton(text="⬅️", callback_data=f"vocab_page_{page - 1}"))
nav_row.append(InlineKeyboardButton(text=f"{page + 1}/{total_pages}", callback_data="vocab_noop"))
if page < total_pages - 1:
nav_row.append(InlineKeyboardButton(text="➡️", callback_data=f"vocab_page_{page + 1}"))
if nav_row:
buttons.append(nav_row)
buttons.append([InlineKeyboardButton(text=t(lang, 'vocab.close_btn'), callback_data="vocab_close")])
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
if edit:
await message_or_callback.message.edit_text(words_list, reply_markup=keyboard)
else:
await message_or_callback.answer(words_list, reply_markup=keyboard)
@router.callback_query(F.data.startswith("vocab_page_"))
async def vocab_page_callback(callback: CallbackQuery):
"""Переключение страницы словаря"""
page = int(callback.data.split("_")[-1])
await callback.answer()
await show_vocabulary_page(callback, page=page, edit=True)
@router.callback_query(F.data == "vocab_noop")
async def vocab_noop_callback(callback: CallbackQuery):
"""Пустой callback для кнопки с номером страницы"""
await callback.answer()
@router.callback_query(F.data == "vocab_close")
async def vocab_close_callback(callback: CallbackQuery):
"""Закрыть словарь"""
await callback.message.delete()
await callback.answer()