feat: мини-истории, слово дня, меню практики

- Добавлены мини-истории для чтения с выбором жанра и вопросами
- Кнопка показа/скрытия перевода истории
- Количество вопросов берётся из настроек пользователя
- Слово дня генерируется глобально в 00:00 UTC
- Кнопка "Практика" открывает меню выбора режима
- Убран автоматический create_all при запуске (только миграции)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-09 15:05:38 +03:00
parent 69c651c031
commit f38ff2f18e
22 changed files with 3131 additions and 77 deletions

415
bot/handlers/exercises.py Normal file
View File

@@ -0,0 +1,415 @@
"""Handler для грамматических упражнений."""
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 services.user_service import UserService
from services.ai_service import ai_service
from utils.i18n import t, get_user_lang, get_user_translation_lang
from utils.levels import get_user_level_for_language
from data.grammar_rules import get_topics_for_current_level_only, get_grammar_topics_for_level
router = Router()
# Количество тем на одной странице
TOPICS_PER_PAGE = 6
class ExercisesStates(StatesGroup):
"""Состояния для грамматических упражнений."""
choosing_topic = State()
viewing_rule = State()
doing_exercise = State()
waiting_answer = State()
@router.message(Command("exercises"))
async def cmd_exercises(message: Message, state: FSMContext):
"""Обработчик команды /exercises."""
await show_exercises_menu(message, state, telegram_id=message.from_user.id)
async def show_exercises_menu(message: Message, state: FSMContext, page: int = 0, telegram_id: int = None):
"""Показать меню выбора темы упражнений."""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, telegram_id)
if not user:
await message.answer(t('ru', 'common.start_first'))
return
lang = get_user_lang(user)
learning_lang = user.learning_language or 'en'
level = get_user_level_for_language(user)
# Получаем темы для текущего уровня
topics = get_topics_for_current_level_only(learning_lang, level)
if not topics:
await message.answer(t(lang, 'exercises.no_topics'))
return
# Пагинация
total_pages = (len(topics) + TOPICS_PER_PAGE - 1) // TOPICS_PER_PAGE
start_idx = page * TOPICS_PER_PAGE
end_idx = start_idx + TOPICS_PER_PAGE
page_topics = topics[start_idx:end_idx]
# Сохраняем в состоянии
await state.update_data(
topics=[t for t in topics], # Все темы
page=page,
level=level,
learning_lang=learning_lang,
user_id=user.id
)
await state.set_state(ExercisesStates.choosing_topic)
# Формируем текст
text = (
t(lang, 'exercises.title') + "\n\n" +
t(lang, 'exercises.your_level', level=level) + "\n\n" +
t(lang, 'exercises.choose_topic')
)
# Формируем клавиатуру
keyboard = []
for topic in page_topics:
# Используем русское название если интерфейс на русском, иначе английское
if lang == 'ru' and topic.get('name_ru'):
btn_text = topic['name_ru']
else:
btn_text = topic['name']
keyboard.append([
InlineKeyboardButton(
text=btn_text,
callback_data=f"exercise_topic_{topic['id']}"
)
])
# Навигация по страницам
nav_row = []
if page > 0:
nav_row.append(InlineKeyboardButton(text="⬅️", callback_data=f"exercises_page_{page - 1}"))
if total_pages > 1:
nav_row.append(InlineKeyboardButton(text=f"{page + 1}/{total_pages}", callback_data="exercises_noop"))
if page < total_pages - 1:
nav_row.append(InlineKeyboardButton(text="➡️", callback_data=f"exercises_page_{page + 1}"))
if nav_row:
keyboard.append(nav_row)
# Кнопка закрыть
keyboard.append([
InlineKeyboardButton(text=t(lang, 'exercises.close_btn'), callback_data="exercises_close")
])
reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
await message.answer(text, reply_markup=reply_markup)
@router.callback_query(F.data.startswith("exercises_page_"))
async def exercises_page_callback(callback: CallbackQuery, state: FSMContext):
"""Переключение страницы тем."""
page = int(callback.data.split("_")[-1])
await callback.answer()
# Удаляем старое сообщение и показываем новое
await callback.message.delete()
await show_exercises_menu(callback.message, state, page=page, telegram_id=callback.from_user.id)
@router.callback_query(F.data == "exercises_noop")
async def exercises_noop_callback(callback: CallbackQuery):
"""Пустой callback для кнопки с номером страницы."""
await callback.answer()
@router.callback_query(F.data == "exercises_close")
async def exercises_close_callback(callback: CallbackQuery, state: FSMContext):
"""Закрыть меню упражнений."""
await callback.message.delete()
await state.clear()
await callback.answer()
@router.callback_query(F.data.startswith("exercise_topic_"))
async def exercise_topic_callback(callback: CallbackQuery, state: FSMContext):
"""Выбор темы для упражнения - показываем правило."""
await callback.answer()
topic_id = callback.data.replace("exercise_topic_", "")
data = await state.get_data()
topics = data.get('topics', [])
level = data.get('level', 'A1')
learning_lang = data.get('learning_lang', 'en')
user_id = data.get('user_id')
# Находим выбранную тему
topic = next((tp for tp in topics if tp['id'] == topic_id), None)
if not topic:
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user) if user else 'ru'
await callback.message.edit_text(t(lang, 'exercises.generate_failed'))
return
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user) if user else 'ru'
# Показываем индикатор генерации правила
await callback.message.edit_text(t(lang, 'exercises.generating_rule'))
# Генерируем объяснение правила
rule_text = await ai_service.generate_grammar_rule(
topic_name=topic['name'],
topic_description=topic.get('description', ''),
level=level,
learning_lang=learning_lang,
ui_lang=lang,
user_id=user_id
)
# Сохраняем данные темы в состоянии
await state.update_data(
current_topic=topic,
rule_text=rule_text
)
await state.set_state(ExercisesStates.viewing_rule)
# Показываем правило с кнопкой "Начать упражнения"
topic_display = topic.get('name_ru', topic['name']) if lang == 'ru' else topic['name']
text = f"📖 <b>{topic_display}</b>\n\n{rule_text}"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t(lang, 'exercises.start_btn'), callback_data="exercises_start_tasks")],
[InlineKeyboardButton(text=t(lang, 'exercises.back_btn'), callback_data="exercises_back_to_topics")]
])
await callback.message.edit_text(text, reply_markup=keyboard)
@router.callback_query(F.data == "exercises_start_tasks", ExercisesStates.viewing_rule)
async def start_exercises_callback(callback: CallbackQuery, state: FSMContext):
"""Начать упражнения после просмотра правила."""
await callback.answer()
data = await state.get_data()
topic = data.get('current_topic', {})
level = data.get('level', 'A1')
learning_lang = data.get('learning_lang', 'en')
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)
lang = get_user_lang(user) if user else 'ru'
translation_lang = get_user_translation_lang(user) if user else 'ru'
tasks_count = getattr(user, 'tasks_count', 5) or 5
# Показываем индикатор генерации
await callback.message.edit_text(t(lang, 'exercises.generating'))
# Генерируем упражнения
exercises = await ai_service.generate_grammar_exercise(
topic_id=topic.get('id', ''),
topic_name=topic.get('name', ''),
topic_description=topic.get('description', ''),
level=level,
learning_lang=learning_lang,
translation_lang=translation_lang,
count=tasks_count,
user_id=user_id
)
if not exercises:
await callback.message.edit_text(t(lang, 'exercises.generate_failed'))
return
# Сохраняем упражнения в состоянии
await state.update_data(
exercises=exercises,
current_exercise=0,
correct_count=0,
topic_name=topic.get('name', ''),
topic_name_ru=topic.get('name_ru', topic.get('name', ''))
)
await state.set_state(ExercisesStates.waiting_answer)
# Показываем первое упражнение
await show_exercise(callback.message, state, lang, edit=True)
async def show_exercise(message: Message, state: FSMContext, lang: str, edit: bool = False):
"""Показать текущее упражнение."""
data = await state.get_data()
exercises = data.get('exercises', [])
current = data.get('current_exercise', 0)
topic_name = data.get('topic_name_ru' if lang == 'ru' else 'topic_name', '')
if current >= len(exercises):
# Все упражнения завершены
await show_results(message, state, lang, edit)
return
exercise = exercises[current]
text = (
t(lang, 'exercises.task_header', topic=topic_name) + "\n\n" +
f"<b>{current + 1}/{len(exercises)}</b>\n\n" +
t(lang, 'exercises.instruction') + "\n\n" +
f"📝 {exercise.get('sentence', '')}\n\n" +
f"💬 <i>{exercise.get('translation', '')}</i>\n\n" +
f"💡 {exercise.get('hint', '')}\n\n" +
t(lang, 'exercises.write_answer')
)
# Кнопки
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t(lang, 'exercises.back_btn'), callback_data="exercises_back_to_topics")],
[InlineKeyboardButton(text=t(lang, 'exercises.close_btn'), callback_data="exercises_close")]
])
if edit:
await message.edit_text(text, reply_markup=keyboard)
else:
await message.answer(text, reply_markup=keyboard)
@router.callback_query(F.data == "exercises_back_to_topics")
async def back_to_topics_callback(callback: CallbackQuery, state: FSMContext):
"""Вернуться к выбору темы."""
await callback.answer()
await callback.message.delete()
data = await state.get_data()
page = data.get('page', 0)
await show_exercises_menu(callback.message, state, page=page, telegram_id=callback.from_user.id)
@router.message(ExercisesStates.waiting_answer)
async def process_answer(message: Message, state: FSMContext):
"""Обработка ответа пользователя."""
user_answer = message.text.strip().lower()
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
lang = get_user_lang(user) if user else 'ru'
data = await state.get_data()
exercises = data.get('exercises', [])
current = data.get('current_exercise', 0)
correct_count = data.get('correct_count', 0)
if current >= len(exercises):
return
exercise = exercises[current]
correct_answer = exercise.get('correct_answer', '').strip().lower()
# Проверяем ответ
is_correct = user_answer == correct_answer
if is_correct:
correct_count += 1
result_text = t(lang, 'exercises.correct') + "\n\n"
else:
result_text = (
t(lang, 'exercises.incorrect') + "\n\n" +
t(lang, 'exercises.your_answer', answer=message.text) + "\n" +
t(lang, 'exercises.right_answer', answer=exercise.get('correct_answer', '')) + "\n\n"
)
# Добавляем объяснение
result_text += t(lang, 'exercises.explanation', text=exercise.get('explanation', ''))
# Обновляем состояние
await state.update_data(
current_exercise=current + 1,
correct_count=correct_count
)
# Кнопка "Следующее" или "Результаты"
if current + 1 < len(exercises):
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t(lang, 'exercises.next_btn'), callback_data="exercises_next")]
])
else:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t(lang, 'exercises.results_btn'), callback_data="exercises_finish")]
])
await message.answer(result_text, reply_markup=keyboard)
@router.callback_query(F.data == "exercises_next")
async def next_exercise_callback(callback: CallbackQuery, state: FSMContext):
"""Переход к следующему упражнению."""
await callback.answer()
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user) if user else 'ru'
await show_exercise(callback.message, state, lang, edit=True)
@router.callback_query(F.data == "exercises_finish")
async def finish_exercises_callback(callback: CallbackQuery, state: FSMContext):
"""Показать результаты."""
await callback.answer()
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user) if user else 'ru'
# Убираем кнопку с предыдущего сообщения (оставляем фидбэк видимым)
await callback.message.edit_reply_markup(reply_markup=None)
# Показываем результаты новым сообщением
await show_results(callback.message, state, lang, edit=False)
async def show_results(message: Message, state: FSMContext, lang: str, edit: bool = False):
"""Показать результаты упражнений."""
data = await state.get_data()
exercises = data.get('exercises', [])
correct_count = data.get('correct_count', 0)
total = len(exercises)
topic_name = data.get('topic_name_ru' if lang == 'ru' else 'topic_name', '')
text = (
f"🎉 <b>{topic_name}</b>\n\n" +
t(lang, 'exercises.score', correct=correct_count, total=total)
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=t(lang, 'exercises.back_btn'), callback_data="exercises_restart")],
[InlineKeyboardButton(text=t(lang, 'exercises.close_btn'), callback_data="exercises_close")]
])
if edit:
await message.edit_text(text, reply_markup=keyboard)
else:
await message.answer(text, reply_markup=keyboard)
await state.clear()
@router.callback_query(F.data == "exercises_restart")
async def restart_exercises_callback(callback: CallbackQuery, state: FSMContext):
"""Вернуться к выбору темы после завершения."""
await callback.answer()
await callback.message.delete()
await show_exercises_menu(callback.message, state, page=0, telegram_id=callback.from_user.id)

View File

@@ -29,6 +29,40 @@ def get_scenario_name(lang: str, scenario: str) -> str:
return t(lang, f'practice.scenario.{scenario}')
async def show_practice_menu(message: Message, telegram_id: int, edit: bool = False):
"""Показать меню выбора сценария практики"""
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, telegram_id)
if not user:
await message.answer(t('ru', 'common.start_first'))
return
lang = get_user_lang(user)
keyboard = []
for scenario_id in SCENARIO_KEYS:
keyboard.append([
InlineKeyboardButton(
text=get_scenario_name(lang, scenario_id),
callback_data=f"scenario_{scenario_id}"
)
])
keyboard.append([
InlineKeyboardButton(
text=t(lang, 'practice.custom_scenario_btn'),
callback_data="scenario_custom"
)
])
reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
if edit:
await message.edit_text(t(lang, 'practice.start_text'), reply_markup=reply_markup)
else:
await message.answer(t(lang, 'practice.start_text'), reply_markup=reply_markup)
@router.message(Command("practice"))
async def cmd_practice(message: Message, state: FSMContext):
"""Обработчик команды /practice"""
@@ -39,31 +73,10 @@ async def cmd_practice(message: Message, state: FSMContext):
await message.answer(t('ru', 'common.start_first'))
return
lang = get_user_lang(user)
# Показываем выбор сценария
keyboard = []
for scenario_id in SCENARIO_KEYS:
keyboard.append([
InlineKeyboardButton(
text=get_scenario_name(lang, scenario_id),
callback_data=f"scenario_{scenario_id}"
)
])
# Кнопка для своего сценария
keyboard.append([
InlineKeyboardButton(
text=t(lang, 'practice.custom_scenario_btn'),
callback_data="scenario_custom"
)
])
reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
await state.update_data(user_id=user.id, level=get_user_level_for_language(user))
await state.set_state(PracticeStates.choosing_scenario)
await message.answer(t(lang, 'practice.start_text'), reply_markup=reply_markup)
await show_practice_menu(message, message.from_user.id, edit=False)
@router.callback_query(F.data == "scenario_custom", PracticeStates.choosing_scenario)

View File

@@ -13,7 +13,7 @@ from aiogram.fsm.state import State, StatesGroup
from database.db import async_session_maker
from services.user_service import UserService
from utils.i18n import t, get_user_translation_lang
from utils.i18n import t, get_user_translation_lang, get_user_lang
from utils.levels import get_user_level_for_language
router = Router()
@@ -57,16 +57,19 @@ def main_menu_keyboard(lang: str = 'ru') -> ReplyKeyboardMarkup:
return ReplyKeyboardMarkup(
resize_keyboard=True,
keyboard=[
[
KeyboardButton(text=t(lang, "menu.add")),
KeyboardButton(text=t(lang, "menu.vocab")),
],
[
KeyboardButton(text=t(lang, "menu.task")),
KeyboardButton(text=t(lang, "menu.practice")),
],
[
KeyboardButton(text=t(lang, "menu.exercises")),
KeyboardButton(text=t(lang, "menu.vocab")),
],
[
KeyboardButton(text=t(lang, "menu.add")),
KeyboardButton(text=t(lang, "menu.stats")),
],
[
KeyboardButton(text=t(lang, "menu.settings")),
],
],
@@ -326,8 +329,74 @@ async def btn_task_pressed(message: Message, state: FSMContext):
@router.message(_menu_match('menu.practice'))
async def btn_practice_pressed(message: Message, state: FSMContext):
from bot.handlers.practice import cmd_practice
await cmd_practice(message, state)
"""Показать меню практики"""
await state.clear()
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
lang = get_user_lang(user) if user else 'ru'
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=f"📖 {t(lang, 'practice_menu.stories')}",
callback_data="practice_stories"
)],
[InlineKeyboardButton(
text=f"💬 {t(lang, 'practice_menu.ai_chat')}",
callback_data="practice_ai"
)],
])
await message.answer(
f"💬 <b>{t(lang, 'practice_menu.title')}</b>\n\n{t(lang, 'practice_menu.choose')}",
reply_markup=keyboard
)
@router.callback_query(F.data == "practice_stories")
async def practice_stories_callback(callback: CallbackQuery, state: FSMContext):
"""Переход к мини-историям"""
await callback.answer()
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user) if user else 'ru'
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text=f"🗣 {t(lang, 'story.genre.dialogue')}", callback_data="story_genre_dialogue"),
InlineKeyboardButton(text=f"📰 {t(lang, 'story.genre.news')}", callback_data="story_genre_news"),
],
[
InlineKeyboardButton(text=f"🎭 {t(lang, 'story.genre.story')}", callback_data="story_genre_story"),
InlineKeyboardButton(text=f"📧 {t(lang, 'story.genre.letter')}", callback_data="story_genre_letter"),
],
[
InlineKeyboardButton(text=f"🍳 {t(lang, 'story.genre.recipe')}", callback_data="story_genre_recipe"),
],
])
await callback.message.edit_text(
f"📖 <b>{t(lang, 'story.title')}</b>\n\n{t(lang, 'story.choose_genre')}",
reply_markup=keyboard
)
@router.callback_query(F.data == "practice_ai")
async def practice_ai_callback(callback: CallbackQuery, state: FSMContext):
"""Переход к AI практике"""
await callback.answer()
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
if user:
from bot.handlers.practice import PracticeStates
from utils.levels import get_user_level_for_language
await state.update_data(user_id=user.id, level=get_user_level_for_language(user))
await state.set_state(PracticeStates.choosing_scenario)
from bot.handlers.practice import show_practice_menu
await show_practice_menu(callback.message, callback.from_user.id, edit=True)
@router.message(_menu_match('menu.import'))
@@ -391,6 +460,14 @@ async def btn_settings_pressed(message: Message):
await cmd_settings(message)
@router.message(_menu_match('menu.exercises'))
async def btn_exercises_pressed(message: Message, state: FSMContext):
"""Показать меню грамматических упражнений."""
from bot.handlers.exercises import show_exercises_menu
await show_exercises_menu(message, state, telegram_id=message.from_user.id)
@router.message(_menu_match('menu.words'))
async def btn_words_pressed(message: Message, state: FSMContext):
"""Подсказать про тематические слова и показать быстрые темы."""

688
bot/handlers/stories.py Normal file
View File

@@ -0,0 +1,688 @@
"""Handler для мини-историй (Reading Practice)."""
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 MiniStory, StoryGenre, 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, get_user_translation_lang
from utils.levels import get_user_level_for_language
router = Router()
class StoryStates(StatesGroup):
"""Состояния для чтения истории"""
reading = State() # Чтение истории
questions = State() # Ответы на вопросы
GENRE_EMOJI = {
"dialogue": "🗣",
"news": "📰",
"story": "🎭",
"letter": "📧",
"recipe": "🍳"
}
def get_genre_keyboard(lang: str) -> InlineKeyboardMarkup:
"""Клавиатура выбора жанра"""
return InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
text=f"🗣 {t(lang, 'story.genre.dialogue')}",
callback_data="story_genre_dialogue"
),
InlineKeyboardButton(
text=f"📰 {t(lang, 'story.genre.news')}",
callback_data="story_genre_news"
),
],
[
InlineKeyboardButton(
text=f"🎭 {t(lang, 'story.genre.story')}",
callback_data="story_genre_story"
),
InlineKeyboardButton(
text=f"📧 {t(lang, 'story.genre.letter')}",
callback_data="story_genre_letter"
),
],
[
InlineKeyboardButton(
text=f"🍳 {t(lang, 'story.genre.recipe')}",
callback_data="story_genre_recipe"
),
],
])
def format_story_text(story: MiniStory, lang: str, show_translation: bool = False) -> str:
"""Форматировать текст истории"""
emoji = GENRE_EMOJI.get(story.genre.value, "📖")
text = f"{emoji} <b>{story.title}</b>\n"
text += f"<i>{t(lang, 'story.level')}: {story.level}{story.word_count} {t(lang, 'story.words')}</i>\n"
text += "" * 20 + "\n\n"
text += story.content
if show_translation and story.translation:
text += "\n\n" + "" * 20
text += f"\n\n🌐 <b>{t(lang, 'story.translation')}:</b>\n\n"
text += f"<i>{story.translation}</i>"
text += "\n\n" + "" * 20
return text
def get_story_keyboard(story_id: int, lang: str, show_translation: bool = False) -> InlineKeyboardMarkup:
"""Клавиатура под историей"""
# Кнопка перевода - показать или скрыть
if show_translation:
translation_btn = InlineKeyboardButton(
text=f"🌐 {t(lang, 'story.hide_translation')}",
callback_data=f"story_hide_translation_{story_id}"
)
else:
translation_btn = InlineKeyboardButton(
text=f"🌐 {t(lang, 'story.show_translation')}",
callback_data=f"story_show_translation_{story_id}"
)
return InlineKeyboardMarkup(inline_keyboard=[
[translation_btn],
[
InlineKeyboardButton(
text=f"📝 {t(lang, 'story.questions_btn')}",
callback_data=f"story_questions_{story_id}"
),
],
[
InlineKeyboardButton(
text=f"📚 {t(lang, 'story.vocab_btn')}",
callback_data=f"story_vocab_{story_id}"
),
],
[
InlineKeyboardButton(
text=f"🔄 {t(lang, 'story.new_btn')}",
callback_data="story_new"
),
],
])
@router.message(Command("story"))
async def cmd_story(message: Message, state: FSMContext):
"""Обработчик команды /story"""
await state.clear()
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
lang = get_user_lang(user)
text = f"📖 <b>{t(lang, 'story.title')}</b>\n\n"
text += t(lang, 'story.choose_genre')
await message.answer(text, reply_markup=get_genre_keyboard(lang))
@router.callback_query(F.data == "story_new")
async def story_new_callback(callback: CallbackQuery, state: FSMContext):
"""Показать выбор жанра для новой истории"""
await callback.answer()
await state.clear()
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user)
text = f"📖 <b>{t(lang, 'story.title')}</b>\n\n"
text += t(lang, 'story.choose_genre')
await callback.message.edit_text(text, reply_markup=get_genre_keyboard(lang))
@router.callback_query(F.data.startswith("story_genre_"))
async def story_genre_callback(callback: CallbackQuery, state: FSMContext):
"""Генерация истории выбранного жанра"""
await callback.answer()
genre = callback.data.replace("story_genre_", "")
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user)
learning_lang = user.learning_language or 'en'
level = get_user_level_for_language(user)
translation_lang = get_user_translation_lang(user)
# Показываем индикатор генерации
await callback.message.edit_text(t(lang, 'story.generating'))
# Получаем количество вопросов из настроек
tasks_count = getattr(user, 'tasks_count', 5) or 5
# Генерируем историю
story_data = await ai_service.generate_mini_story(
genre=genre,
level=level,
learning_lang=learning_lang,
translation_lang=translation_lang,
user_id=user.id,
num_questions=tasks_count
)
if not story_data:
await callback.message.edit_text(
t(lang, 'story.failed'),
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=f"🔄 {t(lang, 'story.try_again')}",
callback_data="story_new"
)]
])
)
return
# Сохраняем историю в БД
story = MiniStory(
user_id=user.id,
title=story_data.get('title', 'Story'),
content=story_data.get('content', ''),
translation=story_data.get('translation', ''),
genre=StoryGenre(genre),
learning_lang=learning_lang,
level=level,
word_count=story_data.get('word_count', 0),
vocabulary=story_data.get('vocabulary', []),
questions=story_data.get('questions', [])
)
session.add(story)
await session.commit()
await session.refresh(story)
# Сохраняем ID истории в состоянии
await state.update_data(story_id=story.id)
await state.set_state(StoryStates.reading)
# Показываем историю
text = format_story_text(story, lang)
await callback.message.edit_text(
text,
reply_markup=get_story_keyboard(story.id, lang)
)
@router.callback_query(F.data.startswith("story_show_translation_"))
async def story_show_translation_callback(callback: CallbackQuery):
"""Показать перевод истории"""
await callback.answer()
story_id = int(callback.data.replace("story_show_translation_", ""))
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user)
story = await session.get(MiniStory, story_id)
if not story:
await callback.answer(t(lang, 'story.not_found'), show_alert=True)
return
text = format_story_text(story, lang, show_translation=True)
await callback.message.edit_text(
text,
reply_markup=get_story_keyboard(story.id, lang, show_translation=True)
)
@router.callback_query(F.data.startswith("story_hide_translation_"))
async def story_hide_translation_callback(callback: CallbackQuery):
"""Скрыть перевод истории"""
await callback.answer()
story_id = int(callback.data.replace("story_hide_translation_", ""))
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user)
story = await session.get(MiniStory, story_id)
if not story:
await callback.answer(t(lang, 'story.not_found'), show_alert=True)
return
text = format_story_text(story, lang, show_translation=False)
await callback.message.edit_text(
text,
reply_markup=get_story_keyboard(story.id, lang, show_translation=False)
)
@router.callback_query(F.data.startswith("story_vocab_"))
async def story_vocab_callback(callback: CallbackQuery):
"""Показать словарь истории"""
await callback.answer()
story_id = int(callback.data.replace("story_vocab_", ""))
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user)
story = await session.get(MiniStory, story_id)
if not story:
await callback.answer(t(lang, 'story.not_found'), show_alert=True)
return
vocabulary = story.vocabulary or []
if not vocabulary:
await callback.answer(t(lang, 'story.no_vocab'), show_alert=True)
return
# Формируем текст со словами
text = f"📚 <b>{t(lang, 'story.vocabulary')}</b>\n\n"
keyboard_buttons = []
for i, word_data in enumerate(vocabulary[:10]):
word = word_data.get('word', '')
translation = word_data.get('translation', '')
transcription = word_data.get('transcription', '')
if transcription:
text += f"• <b>{word}</b> [{transcription}] — {translation}\n"
else:
text += f"• <b>{word}</b> — {translation}\n"
# Кнопка добавления слова
keyboard_buttons.append([
InlineKeyboardButton(
text=f" {word}",
callback_data=f"story_addword_{story_id}_{i}"
)
])
# Кнопка "Добавить все"
keyboard_buttons.append([
InlineKeyboardButton(
text=f" {t(lang, 'story.add_all')}",
callback_data=f"story_addall_{story_id}"
)
])
# Кнопка назад
keyboard_buttons.append([
InlineKeyboardButton(
text=f"⬅️ {t(lang, 'story.back')}",
callback_data=f"story_back_{story_id}"
)
])
await callback.message.edit_text(
text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard_buttons)
)
@router.callback_query(F.data.startswith("story_addword_"))
async def story_addword_callback(callback: CallbackQuery):
"""Добавить одно слово из истории"""
parts = callback.data.split("_")
story_id = int(parts[2])
word_idx = int(parts[3])
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user)
story = await session.get(MiniStory, story_id)
if not story or not story.vocabulary:
await callback.answer(t(lang, 'story.not_found'), show_alert=True)
return
if word_idx >= len(story.vocabulary):
await callback.answer(t(lang, 'story.word_not_found'), show_alert=True)
return
word_data = story.vocabulary[word_idx]
word = word_data.get('word', '')
translation = word_data.get('translation', '')
transcription = word_data.get('transcription')
# Проверяем, нет ли уже
existing = await VocabularyService.get_word_by_original(
session, user.id, word, source_lang=story.learning_lang
)
if existing:
await callback.answer(t(lang, 'words.already_exists', word=word), show_alert=True)
return
# Добавляем слово
translation_lang = get_user_translation_lang(user)
await VocabularyService.add_word(
session=session,
user_id=user.id,
word_original=word,
word_translation=translation,
source_lang=story.learning_lang,
translation_lang=translation_lang,
transcription=transcription,
difficulty_level=story.level,
source=WordSource.IMPORT
)
await session.commit()
await callback.answer(t(lang, 'story.word_added', word=word), show_alert=True)
@router.callback_query(F.data.startswith("story_addall_"))
async def story_addall_callback(callback: CallbackQuery):
"""Добавить все слова из истории"""
story_id = int(callback.data.replace("story_addall_", ""))
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user)
story = await session.get(MiniStory, story_id)
if not story or not story.vocabulary:
await callback.answer(t(lang, 'story.not_found'), show_alert=True)
return
translation_lang = get_user_translation_lang(user)
added = 0
for word_data in story.vocabulary:
word = word_data.get('word', '')
translation = word_data.get('translation', '')
transcription = word_data.get('transcription')
# Проверяем дубликаты
existing = await VocabularyService.get_word_by_original(
session, user.id, word, source_lang=story.learning_lang
)
if not existing:
await VocabularyService.add_word(
session=session,
user_id=user.id,
word_original=word,
word_translation=translation,
source_lang=story.learning_lang,
translation_lang=translation_lang,
transcription=transcription,
difficulty_level=story.level,
source=WordSource.IMPORT
)
added += 1
await session.commit()
await callback.answer(t(lang, 'story.words_added', n=added), show_alert=True)
@router.callback_query(F.data.startswith("story_back_"))
async def story_back_callback(callback: CallbackQuery):
"""Вернуться к истории"""
await callback.answer()
story_id = int(callback.data.replace("story_back_", ""))
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user)
story = await session.get(MiniStory, story_id)
if not story:
await callback.message.edit_text(t(lang, 'story.not_found'))
return
text = format_story_text(story, lang)
await callback.message.edit_text(
text,
reply_markup=get_story_keyboard(story.id, lang)
)
@router.callback_query(F.data.startswith("story_questions_"))
async def story_questions_callback(callback: CallbackQuery, state: FSMContext):
"""Показать вопросы по истории"""
await callback.answer()
story_id = int(callback.data.replace("story_questions_", ""))
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user)
story = await session.get(MiniStory, story_id)
if not story:
await callback.message.edit_text(t(lang, 'story.not_found'))
return
questions = story.questions or []
if not questions:
await callback.answer(t(lang, 'story.no_questions'), show_alert=True)
return
# Сохраняем состояние
await state.update_data(
story_id=story_id,
current_question=0,
correct_answers=0,
total_questions=len(questions)
)
await state.set_state(StoryStates.questions)
# Показываем первый вопрос
await show_question(callback.message, story, 0, lang, edit=True)
async def show_question(message: Message, story: MiniStory, q_idx: int, lang: str, edit: bool = False):
"""Показать вопрос"""
questions = story.questions or []
if q_idx >= len(questions):
return
q = questions[q_idx]
total = len(questions)
text = f"📝 <b>{t(lang, 'story.question')} {q_idx + 1}/{total}</b>\n\n"
text += f"{q.get('question', '')}\n"
# Кнопки с вариантами ответов
options = q.get('options', [])
keyboard_buttons = []
for i, option in enumerate(options):
keyboard_buttons.append([
InlineKeyboardButton(
text=option,
callback_data=f"story_answer_{story.id}_{q_idx}_{i}"
)
])
keyboard = InlineKeyboardMarkup(inline_keyboard=keyboard_buttons)
if edit:
await message.edit_text(text, reply_markup=keyboard)
else:
await message.answer(text, reply_markup=keyboard)
@router.callback_query(F.data.startswith("story_answer_"))
async def story_answer_callback(callback: CallbackQuery, state: FSMContext):
"""Обработка ответа на вопрос"""
parts = callback.data.split("_")
story_id = int(parts[2])
q_idx = int(parts[3])
answer_idx = int(parts[4])
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user)
story = await session.get(MiniStory, story_id)
if not story:
await callback.answer(t(lang, 'story.not_found'), show_alert=True)
return
questions = story.questions or []
if q_idx >= len(questions):
await callback.answer(t(lang, 'story.question_not_found'), show_alert=True)
return
q = questions[q_idx]
correct = q.get('correct', 0)
options = q.get('options', [])
# Получаем данные состояния
data = await state.get_data()
correct_answers = data.get('correct_answers', 0)
total_questions = data.get('total_questions', len(questions))
# Проверяем ответ
is_correct = (answer_idx == correct)
if is_correct:
correct_answers += 1
# Показываем результат ответа
text = f"📝 <b>{t(lang, 'story.question')} {q_idx + 1}/{total_questions}</b>\n\n"
text += f"{q.get('question', '')}\n\n"
for i, option in enumerate(options):
if i == correct:
text += f"{option}\n"
elif i == answer_idx and not is_correct:
text += f"{option}\n"
else:
text += f"{option}\n"
if is_correct:
text += f"\n{t(lang, 'story.correct')}"
await callback.answer("", show_alert=False)
else:
text += f"\n{t(lang, 'story.incorrect')}"
await callback.answer("", show_alert=False)
# Обновляем состояние
await state.update_data(correct_answers=correct_answers)
# Следующий вопрос или результаты
next_q_idx = q_idx + 1
if next_q_idx < total_questions:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=f"➡️ {t(lang, 'story.next_question')}",
callback_data=f"story_nextq_{story_id}_{next_q_idx}"
)]
])
else:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=f"📊 {t(lang, 'story.show_results')}",
callback_data=f"story_results_{story_id}_{correct_answers}"
)]
])
await callback.message.edit_text(text, reply_markup=keyboard)
@router.callback_query(F.data.startswith("story_nextq_"))
async def story_nextq_callback(callback: CallbackQuery, state: FSMContext):
"""Следующий вопрос"""
await callback.answer()
parts = callback.data.split("_")
story_id = int(parts[2])
q_idx = int(parts[3])
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user)
story = await session.get(MiniStory, story_id)
if not story:
await callback.message.edit_text(t(lang, 'story.not_found'))
return
await show_question(callback.message, story, q_idx, lang, edit=True)
@router.callback_query(F.data.startswith("story_results_"))
async def story_results_callback(callback: CallbackQuery, state: FSMContext):
"""Показать результаты"""
await callback.answer()
await state.clear()
parts = callback.data.split("_")
story_id = int(parts[2])
correct_answers = int(parts[3])
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user)
story = await session.get(MiniStory, story_id)
if not story:
await callback.message.edit_text(t(lang, 'story.not_found'))
return
total = len(story.questions or [])
# Обновляем статус истории
story.is_completed = True
story.correct_answers = correct_answers
await session.commit()
# Определяем эмодзи по результату
percentage = (correct_answers / total * 100) if total > 0 else 0
if percentage >= 80:
emoji = "🎉"
comment = t(lang, 'story.result_excellent')
elif percentage >= 50:
emoji = "👍"
comment = t(lang, 'story.result_good')
else:
emoji = "📚"
comment = t(lang, 'story.result_practice')
text = f"{emoji} <b>{t(lang, 'story.results_title')}</b>\n\n"
text += f"📖 {story.title}\n\n"
text += f"{t(lang, 'story.correct_answers')}: <b>{correct_answers}/{total}</b>\n"
text += f"{t(lang, 'story.accuracy')}: <b>{percentage:.0f}%</b>\n\n"
text += f"<i>{comment}</i>"
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(
text=f"📚 {t(lang, 'story.vocab_btn')}",
callback_data=f"story_vocab_{story_id}"
),
],
[
InlineKeyboardButton(
text=f"🔄 {t(lang, 'story.new_btn')}",
callback_data="story_new"
),
],
])
await callback.message.edit_text(text, reply_markup=keyboard)

View File

@@ -18,12 +18,13 @@ class AddWordStates(StatesGroup):
"""Состояния для добавления слова"""
waiting_for_confirmation = State()
waiting_for_word = State()
viewing_batch = State() # Просмотр списка слов для batch-добавления
@router.message(Command("add"))
async def cmd_add(message: Message, state: FSMContext):
"""Обработчик команды /add [слово]"""
# Получаем слово из команды
"""Обработчик команды /add [слово] или /add [слово1, слово2, ...]"""
# Получаем слово(а) из команды
parts = message.text.split(maxsplit=1)
if len(parts) < 2:
@@ -34,15 +35,32 @@ async def cmd_add(message: Message, state: FSMContext):
await state.set_state(AddWordStates.waiting_for_word)
return
word = parts[1].strip()
await process_word_addition(message, state, word)
text = parts[1].strip()
# Проверяем, есть ли несколько слов (через запятую)
if ',' in text:
words = [w.strip() for w in text.split(',') if w.strip()]
if len(words) > 1:
await process_batch_addition(message, state, words)
return
# Одно слово - стандартная обработка
await process_word_addition(message, state, text)
@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)
"""Обработка ввода слова или нескольких слов"""
text = message.text.strip()
# Проверяем, есть ли несколько слов (через запятую)
if ',' in text:
words = [w.strip() for w in text.split(',') if w.strip()]
if len(words) > 1:
await process_batch_addition(message, state, words)
return
await process_word_addition(message, state, text)
async def process_word_addition(message: Message, state: FSMContext, word: str):
@@ -188,6 +206,201 @@ async def cancel_add_word(callback: CallbackQuery, state: FSMContext):
await callback.answer()
# === Batch добавление нескольких слов ===
async def process_batch_addition(message: Message, state: FSMContext, words: list[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
lang = get_user_lang(user)
# Ограничиваем количество слов
if len(words) > 20:
words = words[:20]
await message.answer(t(lang, 'add_batch.truncated', n=20))
# Показываем индикатор загрузки
processing_msg = await message.answer(t(lang, 'add_batch.translating', n=len(words)))
# Получаем переводы через AI batch-методом
source_lang = user.learning_language or 'en'
translation_lang = get_user_translation_lang(user)
translated_words = await ai_service.translate_words_batch(
words=words,
source_lang=source_lang,
translation_lang=translation_lang,
user_id=user.id
)
await processing_msg.delete()
if not translated_words:
await message.answer(t(lang, 'add_batch.failed'))
return
# Сохраняем данные в состоянии
await state.update_data(
batch_words=translated_words,
user_id=user.id
)
await state.set_state(AddWordStates.viewing_batch)
# Показываем список слов
await show_batch_words(message, translated_words, lang)
async def show_batch_words(message: Message, words: list, lang: str):
"""Показать список слов для batch-добавления"""
text = t(lang, 'add_batch.header', n=len(words)) + "\n\n"
for idx, word_data in enumerate(words, 1):
word = word_data.get('word', '')
translation = word_data.get('translation', '')
transcription = word_data.get('transcription', '')
line = f"{idx}. <b>{word}</b>"
if transcription:
line += f" [{transcription}]"
line += f"\n {translation}\n"
text += line
text += "\n" + t(lang, 'add_batch.choose')
# Создаем кнопки для каждого слова (по 2 в ряд)
keyboard = []
for idx, word_data in enumerate(words):
button = InlineKeyboardButton(
text=f" {word_data.get('word', '')[:15]}",
callback_data=f"batch_word_{idx}"
)
if len(keyboard) == 0 or len(keyboard[-1]) == 2:
keyboard.append([button])
else:
keyboard[-1].append(button)
# Кнопка "Добавить все"
keyboard.append([
InlineKeyboardButton(text=t(lang, 'words.add_all_btn'), callback_data="batch_add_all")
])
# Кнопка "Закрыть"
keyboard.append([
InlineKeyboardButton(text=t(lang, 'words.close_btn'), callback_data="batch_close")
])
reply_markup = InlineKeyboardMarkup(inline_keyboard=keyboard)
await message.answer(text, reply_markup=reply_markup)
@router.callback_query(F.data.startswith("batch_word_"), AddWordStates.viewing_batch)
async def batch_add_single(callback: CallbackQuery, state: FSMContext):
"""Добавить одно слово из batch"""
await callback.answer()
word_index = int(callback.data.split("_")[2])
data = await state.get_data()
words = data.get('batch_words', [])
user_id = data.get('user_id')
if word_index >= len(words):
return
word_data = words[word_index]
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user)
# Проверяем, нет ли уже такого слова
existing = await VocabularyService.get_word_by_original(
session, user_id, word_data.get('word', ''), source_lang=user.learning_language
)
if existing:
await callback.answer(t(lang, 'words.already_exists', word=word_data.get('word', '')), show_alert=True)
return
# Добавляем слово
translation_lang = get_user_translation_lang(user)
await VocabularyService.add_word(
session=session,
user_id=user_id,
word_original=word_data.get('word', ''),
word_translation=word_data.get('translation', ''),
source_lang=user.learning_language,
translation_lang=translation_lang,
transcription=word_data.get('transcription'),
source=WordSource.MANUAL
)
await callback.message.answer(t(lang, 'words.added_single', word=word_data.get('word', '')))
@router.callback_query(F.data == "batch_add_all", AddWordStates.viewing_batch)
async def batch_add_all(callback: CallbackQuery, state: FSMContext):
"""Добавить все слова из batch"""
await callback.answer()
data = await state.get_data()
words = data.get('batch_words', [])
user_id = data.get('user_id')
added_count = 0
skipped_count = 0
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
for word_data in words:
# Проверяем, нет ли уже такого слова
existing = await VocabularyService.get_word_by_original(
session, user_id, word_data.get('word', ''), source_lang=user.learning_language
)
if existing:
skipped_count += 1
continue
# Добавляем слово
translation_lang = get_user_translation_lang(user)
await VocabularyService.add_word(
session=session,
user_id=user_id,
word_original=word_data.get('word', ''),
word_translation=word_data.get('translation', ''),
source_lang=user.learning_language,
translation_lang=translation_lang,
transcription=word_data.get('transcription'),
source=WordSource.MANUAL
)
added_count += 1
lang = get_user_lang(user)
result_text = t(lang, 'import.added_count', n=added_count)
if skipped_count > 0:
result_text += "\n" + t(lang, 'import.skipped_count', n=skipped_count)
await callback.message.edit_reply_markup(reply_markup=None)
await callback.message.answer(result_text)
await state.clear()
@router.callback_query(F.data == "batch_close", AddWordStates.viewing_batch)
async def batch_close(callback: CallbackQuery, state: FSMContext):
"""Закрыть batch добавление"""
await callback.message.delete()
await state.clear()
await callback.answer()
WORDS_PER_PAGE = 10

139
bot/handlers/wordofday.py Normal file
View File

@@ -0,0 +1,139 @@
"""Handler для функции 'Слово дня'."""
from datetime import datetime
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from database.db import async_session_maker
from database.models import WordOfDay, WordSource
from services.user_service import UserService
from services.vocabulary_service import VocabularyService
from services.wordofday_service import wordofday_service
from utils.i18n import t, get_user_lang, get_user_translation_lang
from utils.levels import get_user_level_for_language
router = Router()
def format_word_of_day(wod: WordOfDay, lang: str) -> str:
"""Форматировать слово дня для отображения."""
date_str = wod.date.strftime("%d.%m.%Y")
text = f"🌅 <b>{t(lang, 'wod.title')}</b> — {date_str}\n\n"
text += f"📝 <b>{wod.word}</b>\n"
if wod.transcription:
text += f"🔊 [{wod.transcription}]\n"
text += f"\n💬 {wod.translation}\n"
# Примеры
if wod.examples:
text += f"\n📖 <b>{t(lang, 'wod.examples')}:</b>\n"
for ex in wod.examples[:2]:
sentence = ex.get('sentence', '')
translation = ex.get('translation', '')
text += f"• <i>{sentence}</i>\n"
if translation:
text += f" ({translation})\n"
# Синонимы
if wod.synonyms:
text += f"\n🔗 <b>{t(lang, 'wod.synonyms')}:</b> {wod.synonyms}\n"
# Этимология/интересный факт
if wod.etymology:
text += f"\n💡 {wod.etymology}\n"
return text
@router.message(Command("wordofday"))
async def cmd_wordofday(message: Message):
"""Обработчик команды /wordofday."""
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
lang = get_user_lang(user)
learning_lang = user.learning_language or 'en'
level = get_user_level_for_language(user)
# Получаем слово дня из глобальной таблицы
wod = await wordofday_service.get_word_of_day(
learning_lang=learning_lang,
level=level
)
if not wod:
# Слово ещё не сгенерировано - показываем сообщение
await message.answer(t(lang, 'wod.not_available'))
return
# Форматируем и отправляем
text = format_word_of_day(wod, lang)
# Кнопка добавления в словарь
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(
text=t(lang, 'wod.add_btn'),
callback_data=f"wod_add_{wod.id}"
)]
])
await message.answer(text, reply_markup=keyboard)
@router.callback_query(F.data.startswith("wod_add_"))
async def wod_add_callback(callback: CallbackQuery):
"""Добавить слово дня в словарь."""
await callback.answer()
wod_id = int(callback.data.replace("wod_add_", ""))
async with async_session_maker() as session:
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
lang = get_user_lang(user)
# Получаем слово дня
wod = await session.get(WordOfDay, wod_id)
if not wod:
await callback.answer(t(lang, 'wod.not_found'), show_alert=True)
return
# Проверяем, нет ли уже в словаре
existing = await VocabularyService.get_word_by_original(
session, user.id, wod.word, source_lang=wod.learning_lang
)
if existing:
await callback.answer(t(lang, 'words.already_exists', word=wod.word), show_alert=True)
return
# Добавляем в словарь
translation_lang = get_user_translation_lang(user)
await VocabularyService.add_word(
session=session,
user_id=user.id,
word_original=wod.word,
word_translation=wod.translation,
source_lang=wod.learning_lang,
translation_lang=translation_lang,
transcription=wod.transcription,
difficulty_level=wod.level,
source=WordSource.SUGGESTED
)
await session.commit()
# Обновляем сообщение
text = format_word_of_day(wod, lang)
text += f"\n✅ <i>{t(lang, 'wod.added')}</i>"
await callback.message.edit_text(text, reply_markup=None)