from aiogram import Router, F from aiogram.filters import CommandStart, Command from aiogram.types import ( Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery, ReplyKeyboardMarkup, KeyboardButton, ) 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 utils.i18n import t, get_user_translation_lang from utils.levels import get_user_level_for_language router = Router() class OnboardingStates(StatesGroup): """Состояния онбординга для новых пользователей""" choosing_interface_lang = State() choosing_learning_lang = State() choosing_translation_lang = State() def onboarding_interface_keyboard() -> InlineKeyboardMarkup: """Клавиатура выбора языка интерфейса при онбординге""" return InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🇷🇺 Русский", callback_data="onboard_interface_ru")], [InlineKeyboardButton(text="🇬🇧 English", callback_data="onboard_interface_en")], [InlineKeyboardButton(text="🇯🇵 日本語", callback_data="onboard_interface_ja")], ]) def onboarding_learning_keyboard(lang: str) -> InlineKeyboardMarkup: """Клавиатура выбора языка изучения при онбординге""" return InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=t(lang, 'onboarding.lang_en'), callback_data="onboard_learning_en")], [InlineKeyboardButton(text=t(lang, 'onboarding.lang_ja'), callback_data="onboard_learning_ja")], ]) def onboarding_translation_keyboard(lang: str) -> InlineKeyboardMarkup: """Клавиатура выбора языка перевода при онбординге""" return InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=t(lang, 'settings.lang_name.ru'), callback_data="onboard_translation_ru")], [InlineKeyboardButton(text=t(lang, 'settings.lang_name.en'), callback_data="onboard_translation_en")], [InlineKeyboardButton(text=t(lang, 'settings.lang_name.ja'), callback_data="onboard_translation_ja")], ]) 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.stats")), KeyboardButton(text=t(lang, "menu.settings")), ], ], ) @router.message(CommandStart()) async def cmd_start(message: Message, state: FSMContext): """Обработчик команды /start""" async with async_session_maker() as session: # Проверяем, существует ли пользователь existing_user = await UserService.get_user_by_telegram_id(session, message.from_user.id) is_new_user = existing_user is None if is_new_user: # Новый пользователь - начинаем онбординг # Сначала создаём пользователя с дефолтными значениями user = await UserService.get_or_create_user( session, telegram_id=message.from_user.id, username=message.from_user.username ) # Приветствие и первый вопрос - язык интерфейса await message.answer( f"👋 Welcome! / Привет! / ようこそ!\n\n" "🌐 Choose your interface language:\n" "🌐 Выбери язык интерфейса:\n" "🌐 インターフェース言語を選択:", reply_markup=onboarding_interface_keyboard() ) await state.set_state(OnboardingStates.choosing_interface_lang) return # Существующий пользователь user = existing_user lang = (user.language_interface or 'ru') await message.answer( t(lang, "start.return", first_name=message.from_user.first_name), reply_markup=main_menu_keyboard(lang), ) # === Обработчики онбординга === @router.callback_query(F.data.startswith("onboard_interface_"), OnboardingStates.choosing_interface_lang) async def onboard_set_interface(callback: CallbackQuery, state: FSMContext): """Установить язык интерфейса при онбординге""" lang = callback.data.split("_")[-1] # ru | en | ja async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) if user: await UserService.update_user_language(session, user.id, lang) await state.update_data(interface_lang=lang) # Второй вопрос - язык изучения await callback.message.edit_text( t(lang, 'onboarding.step2_title'), reply_markup=onboarding_learning_keyboard(lang) ) await state.set_state(OnboardingStates.choosing_learning_lang) await callback.answer() @router.callback_query(F.data.startswith("onboard_learning_"), OnboardingStates.choosing_learning_lang) async def onboard_set_learning(callback: CallbackQuery, state: FSMContext): """Установить язык изучения при онбординге""" learning_lang = callback.data.split("_")[-1] # en | ja data = await state.get_data() lang = data.get('interface_lang', 'ru') async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) if user: await UserService.update_user_learning_language(session, user.id, learning_lang) await state.update_data(learning_lang=learning_lang) # Третий вопрос - язык перевода await callback.message.edit_text( t(lang, 'onboarding.step3_title'), reply_markup=onboarding_translation_keyboard(lang) ) await state.set_state(OnboardingStates.choosing_translation_lang) await callback.answer() @router.callback_query(F.data.startswith("onboard_translation_"), OnboardingStates.choosing_translation_lang) async def onboard_set_translation(callback: CallbackQuery, state: FSMContext): """Установить язык перевода при онбординге и завершить""" translation_lang = callback.data.split("_")[-1] # ru | en | ja data = await state.get_data() lang = data.get('interface_lang', 'ru') async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) if user: await UserService.update_user_translation_language(session, user.id, translation_lang) await state.clear() # Приветствие с выбранными настройками await callback.message.edit_text(t(lang, 'onboarding.complete')) # Показываем главное меню и предлагаем тест уровня await callback.message.answer( t(lang, "start.new_intro", first_name=callback.from_user.first_name), reply_markup=main_menu_keyboard(lang), ) keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=t(lang, 'start.offer_btn'), callback_data="offer_level_test")], [InlineKeyboardButton(text=t(lang, 'start.skip_btn'), callback_data="skip_level_test")] ]) await callback.message.answer(t(lang, "start.offer_test"), reply_markup=keyboard) await callback.answer() @router.message(Command("menu")) async def cmd_menu(message: Message): """Показать клавиатуру с основными командами.""" # Определяем язык пользователя 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, "menu.below"), reply_markup=main_menu_keyboard(lang)) # Обработчики кнопок главного меню (по тексту) def _menu_match(key: str): labels = {t('ru', key), t('en', key), t('ja', key)} return lambda m: m.text in labels @router.message(_menu_match('menu.add')) async def btn_add_pressed(message: Message, state: FSMContext): """Показать меню добавления слов""" 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' keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=t(lang, 'add_menu.manual'), callback_data="add_manual")], [InlineKeyboardButton(text=t(lang, 'add_menu.thematic'), callback_data="add_thematic")], [InlineKeyboardButton(text=t(lang, 'add_menu.import'), callback_data="add_import")] ]) await message.answer(t(lang, 'add_menu.title'), reply_markup=keyboard) @router.callback_query(F.data == "add_manual") async def add_manual_callback(callback: CallbackQuery, state: FSMContext): """Добавить слово вручную""" await callback.answer() from bot.handlers.vocabulary import AddWordStates 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 state.set_state(AddWordStates.waiting_for_word) await callback.message.edit_text(t(lang, 'add.prompt')) @router.callback_query(F.data == "add_thematic") async def add_thematic_callback(callback: CallbackQuery): """Тематические слова""" await callback.answer() 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' # Показываем подсказку по использованию /words text = ( t(lang, 'words.help_title') + "\n\n" + t(lang, 'words.help_usage') + "\n\n" + t(lang, 'words.help_examples') + "\n\n" + t(lang, 'words.help_note') ) # Популярные темы как кнопки keyboard = InlineKeyboardMarkup(inline_keyboard=[ [ InlineKeyboardButton(text=t(lang, 'words.topic_travel'), callback_data="words_travel"), InlineKeyboardButton(text=t(lang, 'words.topic_food'), callback_data="words_food") ], [ InlineKeyboardButton(text=t(lang, 'words.topic_work'), callback_data="words_work"), InlineKeyboardButton(text=t(lang, 'words.topic_technology'), callback_data="words_technology") ], [InlineKeyboardButton(text="⬅️ " + t(lang, 'settings.back'), callback_data="back_to_add_menu")] ]) await callback.message.edit_text(text, reply_markup=keyboard) @router.callback_query(F.data.startswith("words_")) async def words_topic_callback(callback: CallbackQuery, state: FSMContext): """Генерация слов по теме""" topic = callback.data.replace("words_", "") await callback.answer() await callback.message.delete() from bot.handlers.words import generate_words_for_theme await generate_words_for_theme(callback.message, state, topic, callback.from_user.id) @router.callback_query(F.data == "add_import") async def add_import_callback(callback: CallbackQuery): """Показать меню импорта""" await callback.answer() 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' keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=t(lang, 'import_menu.from_text'), callback_data="import_from_text")], [InlineKeyboardButton(text=t(lang, 'import_menu.from_file'), callback_data="import_from_file")], [InlineKeyboardButton(text="⬅️ " + t(lang, 'settings.back'), callback_data="back_to_add_menu")] ]) await callback.message.edit_text(t(lang, 'import_menu.title'), reply_markup=keyboard) @router.callback_query(F.data == "back_to_add_menu") async def back_to_add_menu_callback(callback: CallbackQuery): """Вернуться в меню добавления""" await callback.answer() 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' keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=t(lang, 'add_menu.manual'), callback_data="add_manual")], [InlineKeyboardButton(text=t(lang, 'add_menu.thematic'), callback_data="add_thematic")], [InlineKeyboardButton(text=t(lang, 'add_menu.import'), callback_data="add_import")] ]) await callback.message.edit_text(t(lang, 'add_menu.title'), reply_markup=keyboard) @router.message(_menu_match('menu.vocab')) async def btn_vocab_pressed(message: Message): from bot.handlers.vocabulary import cmd_vocabulary await cmd_vocabulary(message) @router.message(_menu_match('menu.task')) async def btn_task_pressed(message: Message, state: FSMContext): from bot.handlers.tasks import cmd_task await cmd_task(message, state) @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) @router.message(_menu_match('menu.import')) async def btn_import_pressed(message: Message, state: FSMContext): """Показать меню импорта""" 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' keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=t(lang, 'import_menu.from_text'), callback_data="import_from_text")], [InlineKeyboardButton(text=t(lang, 'import_menu.from_file'), callback_data="import_from_file")] ]) await message.answer(t(lang, 'import_menu.title'), reply_markup=keyboard) @router.callback_query(F.data == "import_from_text") async def import_from_text_callback(callback: CallbackQuery, state: FSMContext): """Импорт из текста""" await callback.answer() from bot.handlers.import_text import ImportStates async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) if not user: await callback.message.edit_text(t('ru', 'common.start_first')) return lang = user.language_interface or 'ru' await state.set_state(ImportStates.waiting_for_text) await callback.message.edit_text( t(lang, 'import.title') + "\n\n" + t(lang, 'import.desc') + "\n\n" + t(lang, 'import.can_send') + "\n\n" + t(lang, 'import.cancel_hint') ) @router.callback_query(F.data == "import_from_file") async def import_from_file_callback(callback: CallbackQuery): """Импорт из файла""" await callback.answer() 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, 'import_menu.file_hint')) @router.message(_menu_match('menu.stats')) async def btn_stats_pressed(message: Message): from bot.handlers.tasks import cmd_stats await cmd_stats(message) @router.message(_menu_match('menu.settings')) async def btn_settings_pressed(message: Message): from bot.handlers.settings import cmd_settings await cmd_settings(message) @router.message(_menu_match('menu.words')) async def btn_words_pressed(message: Message, state: FSMContext): """Подсказать про тематические слова и показать быстрые темы.""" from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 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' text = ( t(lang, 'words.help_title') + "\n\n" + t(lang, 'words.help_usage') + "\n\n" + t(lang, 'words.popular') ) keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text=t(lang, 'words.topic_travel'), callback_data="menu_theme_travel")], [InlineKeyboardButton(text=t(lang, 'words.topic_food'), callback_data="menu_theme_food")], [InlineKeyboardButton(text=t(lang, 'words.topic_work'), callback_data="menu_theme_work")], [InlineKeyboardButton(text=t(lang, 'words.topic_nature'), callback_data="menu_theme_nature")], [InlineKeyboardButton(text=t(lang, 'words.topic_technology'), callback_data="menu_theme_technology")], ]) await message.answer(text, reply_markup=keyboard) @router.callback_query(F.data.startswith("menu_theme_")) async def pick_theme_from_menu(callback: CallbackQuery, state: FSMContext): """Сгенерировать слова по выбранной теме из меню и показать список.""" from database.db import async_session_maker from services.user_service import UserService from services.ai_service import ai_service from bot.handlers.words import show_words_list, WordsStates # Сразу отвечаем на callback, чтобы избежать таймаута await callback.answer() theme = callback.data.split("menu_theme_")[-1] async with async_session_maker() as session: user = await UserService.get_user_by_telegram_id(session, callback.from_user.id) if not user: await callback.answer(t('ru', 'common.start_first'), show_alert=True) return lang = (user.language_interface or 'ru') current_level = get_user_level_for_language(user) generating = await callback.message.answer(t(lang, 'words.generating', theme=theme)) words = await ai_service.generate_thematic_words( theme=theme, level=current_level, count=10, learning_lang=user.learning_language, translation_lang=get_user_translation_lang(user), user_id=user.id ) await generating.delete() if not words: await callback.message.answer(t(lang, 'words.generate_failed')) return # Сохраняем в состояние как в /words await state.update_data(theme=theme, words=words, user_id=user.id, level=current_level) await state.set_state(WordsStates.viewing_words) await show_words_list(callback.message, words, theme, user.id) @router.message(Command("help")) async def cmd_help(message: Message): """Обработчик команды /help""" # Определяем язык пользователя 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, "start.help")) @router.callback_query(F.data == "offer_level_test") async def offer_level_test_callback(callback: CallbackQuery, state: FSMContext): """Начать тест уровня из приветствия""" from bot.handlers.level_test import start_level_test await callback.message.delete() await start_level_test(callback.message, state, telegram_id=callback.from_user.id) await callback.answer() @router.callback_query(F.data == "skip_level_test") async def skip_level_test_callback(callback: CallbackQuery): """Пропустить тест уровня""" 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, 'start.skip_msg')) await callback.answer()