From 6c34219a6fb3988147465440cb1587c13547c1d0 Mon Sep 17 00:00:00 2001 From: ruslangilfanov Date: Sat, 20 Dec 2025 13:17:12 +0200 Subject: [PATCH] add "add word" dialog with handlers, windows, and states integration --- src/greek_lang/glossaries/models.py | 3 +- src/greek_lang/languages.py | 2 +- src/greek_lang/srs/models.py | 1 + src/greek_lang/tg_bot/dialogs/__init__.py | 3 + .../tg_bot/dialogs/add_word/__init__.py | 0 .../tg_bot/dialogs/add_word/handlers.py | 65 +++++++++++++++++ .../tg_bot/dialogs/add_word/windows.py | 73 +++++++++++++++++++ .../tg_bot/dialogs/base_handlers.py | 16 ++++ .../tg_bot/dialogs/main_menu/handlers.py | 26 +++++++ .../tg_bot/dialogs/main_menu/windows.py | 13 +++- src/greek_lang/tg_bot/dialogs/states.py | 2 + 11 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 src/greek_lang/tg_bot/dialogs/add_word/__init__.py create mode 100644 src/greek_lang/tg_bot/dialogs/add_word/handlers.py create mode 100644 src/greek_lang/tg_bot/dialogs/add_word/windows.py create mode 100644 src/greek_lang/tg_bot/dialogs/base_handlers.py diff --git a/src/greek_lang/glossaries/models.py b/src/greek_lang/glossaries/models.py index d19b4a0..1bd7a4d 100644 --- a/src/greek_lang/glossaries/models.py +++ b/src/greek_lang/glossaries/models.py @@ -11,7 +11,8 @@ from ..database.base import Base from ..languages import LanguageEnum -class LexicalCategoryEnum(str, enum.Enum): +@enum.unique +class LexicalCategoryEnum(enum.StrEnum): noun = "noun" verb = "verb" adjective = "adjective" diff --git a/src/greek_lang/languages.py b/src/greek_lang/languages.py index 51a44fe..857b651 100644 --- a/src/greek_lang/languages.py +++ b/src/greek_lang/languages.py @@ -2,7 +2,7 @@ import enum @enum.unique -class LanguageEnum(str, enum.Enum): +class LanguageEnum(enum.StrEnum): ru = "ru" en = "en" el = "el" diff --git a/src/greek_lang/srs/models.py b/src/greek_lang/srs/models.py index 7057a69..f4d49ff 100644 --- a/src/greek_lang/srs/models.py +++ b/src/greek_lang/srs/models.py @@ -16,6 +16,7 @@ from sqlalchemy.orm import Mapped, mapped_column from greek_lang.database.base import Base +@enum.unique class ReviewState(enum.StrEnum): learning = "learning" review = "review" diff --git a/src/greek_lang/tg_bot/dialogs/__init__.py b/src/greek_lang/tg_bot/dialogs/__init__.py index c0dd330..914dbae 100644 --- a/src/greek_lang/tg_bot/dialogs/__init__.py +++ b/src/greek_lang/tg_bot/dialogs/__init__.py @@ -1,8 +1,11 @@ from aiogram_dialog import Dialog from .main_menu import windows as main_windows +from .add_word import windows as add_word_windows dialog = Dialog( main_windows.main_window, + add_word_windows.add_word_window, + add_word_windows.add_word_result_window, ) diff --git a/src/greek_lang/tg_bot/dialogs/add_word/__init__.py b/src/greek_lang/tg_bot/dialogs/add_word/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/greek_lang/tg_bot/dialogs/add_word/handlers.py b/src/greek_lang/tg_bot/dialogs/add_word/handlers.py new file mode 100644 index 0000000..703f084 --- /dev/null +++ b/src/greek_lang/tg_bot/dialogs/add_word/handlers.py @@ -0,0 +1,65 @@ +from aiogram.types import Message, CallbackQuery, BufferedInputFile +from aiogram_dialog import DialogManager +from aiogram_dialog.widgets.input import MessageInput +from aiogram_dialog.widgets.kbd import Button +from greek_lang.languages import LanguageEnum +from greek_lang.translator import translate +from ..states import States + + +async def add_word( + message: Message, + source: MessageInput | Button, + manager: DialogManager, +) -> None: + if not message.text: + return + word = message.text.strip() + if not word: + return + source_lang = LanguageEnum.ru + target_lang = LanguageEnum.el + glossary_word = await translate(word, source_lang, target_lang=target_lang) + + # Try to send audio pronunciation back to the user + try: + audio_bytes = getattr(glossary_word, "audio_file", None) + if audio_bytes: + # aiogTTS produces MP3 data; send as audio + caption = ( + f"{glossary_word.term}{glossary_word.translation}" + ) + input_file = BufferedInputFile( + audio_bytes, filename=f"{glossary_word.term}.mp3" + ) + await message.answer_audio( + audio=input_file, caption=caption, parse_mode="HTML" + ) + except Exception: + # Silently ignore audio sending issues to not break the flow + pass + + # Store data for the result window + manager.dialog_data.update( + { + "term": glossary_word.term, + "translation": glossary_word.translation, + "transcription": glossary_word.transcription or "", + "lexical_category": getattr(glossary_word, "lexical_category", ""), + "description": glossary_word.description or "", + "example": glossary_word.example or "", + "note": glossary_word.note or "", + } + ) + + # Switch to the result window state + await manager.switch_to(States.add_word_result) + + +async def on_add_another( + callback: CallbackQuery, + button: Button, + manager: DialogManager, +) -> None: + await callback.answer() + await manager.switch_to(States.add_word) diff --git a/src/greek_lang/tg_bot/dialogs/add_word/windows.py b/src/greek_lang/tg_bot/dialogs/add_word/windows.py new file mode 100644 index 0000000..441fe37 --- /dev/null +++ b/src/greek_lang/tg_bot/dialogs/add_word/windows.py @@ -0,0 +1,73 @@ +from typing import Any + +from aiogram.enums import ParseMode +from aiogram_dialog import DialogManager, Window +from aiogram_dialog.widgets.input import MessageInput +from aiogram_dialog.widgets.kbd import Button, Row +from aiogram_dialog.widgets.markup.reply_keyboard import ReplyKeyboardFactory +from aiogram_dialog.widgets.text import Format, Const + +from ..states import States +from ..base_handlers import cancel_handler +from . import handlers + + +async def add_word_window_getter( + dialog_manager: DialogManager, **kwargs: Any +) -> dict[str, Any]: + return {} + + +add_word_window = Window( + Const("Введите слово:"), + MessageInput(func=handlers.add_word), + Row( + Button( + Format("❌ Отмена"), + on_click=cancel_handler, # type: ignore[arg-type] + id="cancel_add_word", + ), + ), + markup_factory=ReplyKeyboardFactory( + input_field_placeholder=Format("Слово..."), + resize_keyboard=True, + one_time_keyboard=True, + ), + state=States.add_word, + parse_mode=ParseMode.HTML, + getter=add_word_window_getter, +) + + +async def add_word_result_getter( + dialog_manager: DialogManager, **kwargs: Any +) -> dict[str, Any]: + # Expose dialog_data fields directly for Format widgets + return dialog_manager.dialog_data + + +add_word_result_window = Window( + Format( + "✅ Слово добавлено!\n\n" + "{term}{translation}\n" + "{transcription}\n" + "Часть речи: {lexical_category!s}\n" + "Описание: {description}\n" + "Пример: {example}" + ), + Row( + Button( + Const("➕ Добавить ещё"), + id="add_another", + on_click=handlers.on_add_another, + ), + Button( + Const("🏠 В меню"), + on_click=cancel_handler, # type: ignore[arg-type] + id="to_main_menu", + ), + ), + state=States.add_word_result, + parse_mode=ParseMode.HTML, + getter=add_word_result_getter, +) diff --git a/src/greek_lang/tg_bot/dialogs/base_handlers.py b/src/greek_lang/tg_bot/dialogs/base_handlers.py new file mode 100644 index 0000000..b153988 --- /dev/null +++ b/src/greek_lang/tg_bot/dialogs/base_handlers.py @@ -0,0 +1,16 @@ +from aiogram_dialog import DialogManager, ShowMode +from aiogram_dialog.api.internal import ReplyCallbackQuery +from aiogram_dialog.widgets.kbd import Cancel + +from .states import States + + +async def cancel_handler( + callback_query: ReplyCallbackQuery, + button: Cancel, + manager: DialogManager, +) -> None: + await manager.switch_to( + States.main_menu, + show_mode=ShowMode.DELETE_AND_SEND, + ) diff --git a/src/greek_lang/tg_bot/dialogs/main_menu/handlers.py b/src/greek_lang/tg_bot/dialogs/main_menu/handlers.py index e69de29..8c91b5d 100644 --- a/src/greek_lang/tg_bot/dialogs/main_menu/handlers.py +++ b/src/greek_lang/tg_bot/dialogs/main_menu/handlers.py @@ -0,0 +1,26 @@ +from aiogram.types import CallbackQuery +from aiogram_dialog import DialogManager, ShowMode +from aiogram_dialog.api.internal import ReplyCallbackQuery +from aiogram_dialog.widgets.kbd import Cancel, Button + +from ..states import States + + +async def on_add_word( + callback: CallbackQuery, + button: Button, + manager: DialogManager, +) -> None: + await callback.answer() + await manager.switch_to(States.add_word) + + +async def cancel_handler( + callback_query: ReplyCallbackQuery, + button: Cancel, + manager: DialogManager, +) -> None: + await manager.switch_to( + States.add_word, + show_mode=ShowMode.DELETE_AND_SEND, + ) diff --git a/src/greek_lang/tg_bot/dialogs/main_menu/windows.py b/src/greek_lang/tg_bot/dialogs/main_menu/windows.py index 1520c79..7a88633 100644 --- a/src/greek_lang/tg_bot/dialogs/main_menu/windows.py +++ b/src/greek_lang/tg_bot/dialogs/main_menu/windows.py @@ -2,9 +2,11 @@ from typing import Any from aiogram.enums import ParseMode from aiogram_dialog import DialogManager, Window -from aiogram_dialog.widgets.text import Format +from aiogram_dialog.widgets.kbd import Row, Button +from aiogram_dialog.widgets.text import Format, Const from ..states import States +from . import handlers async def main_getter(dialog_manager: DialogManager, **kwargs: Any) -> dict[str, Any]: @@ -13,10 +15,17 @@ async def main_getter(dialog_manager: DialogManager, **kwargs: Any) -> dict[str, main_window = Window( Format( - "Выбери действие:", + "Выбери действие:", when=lambda data, widget, dialog_manager: data["dialog_data"].get("action") is None, ), + Row( + Button( + Const("Добавить слово"), + id="add_word", + on_click=handlers.on_add_word, + ), + ), state=States.main_menu, getter=main_getter, parse_mode=ParseMode.HTML, diff --git a/src/greek_lang/tg_bot/dialogs/states.py b/src/greek_lang/tg_bot/dialogs/states.py index 6914ee5..9f74921 100644 --- a/src/greek_lang/tg_bot/dialogs/states.py +++ b/src/greek_lang/tg_bot/dialogs/states.py @@ -3,3 +3,5 @@ from aiogram.fsm.state import State, StatesGroup class States(StatesGroup): main_menu = State() + add_word = State() + add_word_result = State()