diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ea85242 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# Telegram Bot Token (получить у @BotFather) +BOT_TOKEN=your_telegram_bot_token_here + +# OpenAI API Key +OPENAI_API_KEY=your_openai_api_key_here + +# Database +DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/language_bot + +# Settings +DEBUG=True diff --git a/README.md b/README.md index 70f2923..e8d6366 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,141 @@ -# tg_bot_language +# Telegram Бот для изучения языков -Бот для изучения иностранных языков \ No newline at end of file +Интеллектуальный Telegram бот для изучения английского языка с использованием AI. + +## Возможности + +- 📚 Управление словарным запасом с автоматическим переводом через AI +- ✍️ Ежедневные задания для практики (в разработке) +- 💬 Диалоговая практика с ИИ (в разработке) +- 📊 Статистика прогресса (в разработке) + +## Текущая версия (MVP) + +**Реализовано:** +- ✅ Команда `/start` - приветствие и регистрация пользователя +- ✅ Команда `/add [слово]` - добавление слов в словарь с AI-переводом +- ✅ Команда `/vocabulary` - просмотр словаря +- ✅ Команда `/help` - справка +- ✅ База данных (PostgreSQL) для хранения пользователей и словарей +- ✅ Интеграция с OpenAI API для перевода слов + +## Установка и запуск + +### 1. Клонирование репозитория + +```bash +git clone http://103.137.249.134:3000/NANDI/tg_bot_language.git +cd tg_bot_language +``` + +### 2. Установка зависимостей + +```bash +pip install -r requirements.txt +``` + +### 3. Настройка окружения + +Скопируйте `.env.example` в `.env`: + +```bash +cp .env.example .env +``` + +Отредактируйте `.env` и заполните необходимые параметры: + +```env +BOT_TOKEN=your_telegram_bot_token_here +OPENAI_API_KEY=your_openai_api_key_here +DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/language_bot +DEBUG=True +``` + +**Получение токенов:** +- Telegram Bot Token: создайте бота через [@BotFather](https://t.me/BotFather) +- OpenAI API Key: получите на [platform.openai.com](https://platform.openai.com/api-keys) + +### 4. Настройка базы данных + +Создайте PostgreSQL базу данных: + +```bash +createdb language_bot +``` + +Или используйте Docker: + +```bash +docker run --name language-bot-db -e POSTGRES_PASSWORD=password -e POSTGRES_DB=language_bot -p 5432:5432 -d postgres:15 +``` + +### 5. Запуск бота + +```bash +python main.py +``` + +## Структура проекта + +``` +bot_tg_language/ +├── bot/ +│ ├── handlers/ # Обработчики команд +│ │ ├── start.py # /start, /help +│ │ └── vocabulary.py # /add, /vocabulary +│ └── keyboards/ # Клавиатуры (пока не используется) +├── database/ +│ ├── models.py # Модели БД (User, Vocabulary, Task) +│ └── db.py # Подключение к БД +├── services/ +│ ├── ai_service.py # Сервис для работы с OpenAI +│ ├── user_service.py # Сервис пользователей +│ └── vocabulary_service.py # Сервис словаря +├── config/ +│ └── settings.py # Настройки приложения +├── main.py # Точка входа +├── requirements.txt # Зависимости +├── .env.example # Пример конфигурации +└── TZ.md # Техническое задание +``` + +## Использование + +### Команды бота + +- `/start` - Начать работу с ботом +- `/add [слово]` - Добавить слово в словарь +- `/vocabulary` - Посмотреть свой словарь +- `/help` - Показать справку + +### Пример использования + +1. Запустите бота: `/start` +2. Добавьте слово: `/add elephant` +3. Бот переведёт слово через AI и предложит добавить в словарь +4. Подтвердите добавление +5. Просмотрите словарь: `/vocabulary` + +## Roadmap + +См. [TZ.md](TZ.md) для полного технического задания. + +**Следующие этапы:** +- [ ] Ежедневные задания с разными типами упражнений +- [ ] Тематические подборки слов +- [ ] Импорт слов из текста +- [ ] Диалоговая практика с AI +- [ ] Статистика и прогресс +- [ ] Spaced repetition алгоритм + +## Технологии + +- **Python 3.11+** +- **aiogram 3.x** - Telegram Bot framework +- **SQLAlchemy 2.x** - ORM для работы с БД +- **PostgreSQL** - База данных +- **OpenAI API** - AI для перевода и проверки + +## Лицензия + +MIT \ No newline at end of file diff --git a/bot/__init__.py b/bot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/handlers/__init__.py b/bot/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/handlers/start.py b/bot/handlers/start.py new file mode 100644 index 0000000..ed22caa --- /dev/null +++ b/bot/handlers/start.py @@ -0,0 +1,66 @@ +from aiogram import Router, F +from aiogram.filters import CommandStart, Command +from aiogram.types import Message +from aiogram.fsm.context import FSMContext + +from database.db import async_session_maker +from services.user_service import UserService + +router = Router() + + +@router.message(CommandStart()) +async def cmd_start(message: Message, state: FSMContext): + """Обработчик команды /start""" + async with async_session_maker() as session: + user = await UserService.get_or_create_user( + session, + telegram_id=message.from_user.id, + username=message.from_user.username + ) + + if user.created_at.timestamp() > (message.date.timestamp() - 60): + # Новый пользователь (создан менее минуты назад) + await message.answer( + f"👋 Привет, {message.from_user.first_name}!\n\n" + f"Я бот для изучения английского языка. Помогу тебе:\n" + f"📚 Пополнять словарный запас\n" + f"✍️ Выполнять ежедневные задания\n" + f"💬 Практиковать язык в диалоге\n\n" + f"Основные команды:\n" + f"/add [слово] - добавить слово в словарь\n" + f"/vocabulary - мой словарь\n" + f"/task - получить задание\n" + f"/stats - статистика\n" + f"/help - справка\n\n" + f"Давай начнём! Отправь мне слово, которое хочешь выучить, или используй команду /add" + ) + else: + # Существующий пользователь + await message.answer( + f"С возвращением, {message.from_user.first_name}! 👋\n\n" + f"Готов продолжить обучение?\n" + f"/vocabulary - посмотреть словарь\n" + f"/task - получить задание\n" + f"/stats - статистика" + ) + + +@router.message(Command("help")) +async def cmd_help(message: Message): + """Обработчик команды /help""" + await message.answer( + "📖 Справка по командам:\n\n" + "Управление словарём:\n" + "/add [слово] - добавить слово в словарь\n" + "/vocabulary - просмотр словаря\n" + "/import - импортировать слова из текста\n\n" + "Обучение:\n" + "/task - получить задание\n" + "/practice - практика с ИИ\n\n" + "Статистика:\n" + "/stats - твой прогресс\n\n" + "Настройки:\n" + "/settings - настройки бота\n\n" + "Ты также можешь просто отправить мне слово, и я предложу добавить его в словарь!" + ) diff --git a/bot/handlers/vocabulary.py b/bot/handlers/vocabulary.py new file mode 100644 index 0000000..b833530 --- /dev/null +++ b/bot/handlers/vocabulary.py @@ -0,0 +1,191 @@ +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 + +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: + await message.answer( + "Отправь слово, которое хочешь добавить:\n" + "Например: /add elephant\n\n" + "Или просто отправь слово без команды!" + ) + 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("Сначала запусти бота командой /start") + return + + # Проверяем, есть ли уже такое слово + existing_word = await VocabularyService.find_word(session, user.id, word) + if existing_word: + await message.answer( + f"Слово '{word}' уже есть в твоём словаре!\n" + f"Перевод: {existing_word.word_translation}" + ) + await state.clear() + return + + # Показываем индикатор загрузки + processing_msg = await message.answer("⏳ Ищу перевод и примеры...") + + # Получаем перевод через AI + word_data = await ai_service.translate_word(word) + + # Удаляем сообщение о загрузке + await processing_msg.delete() + + # Формируем примеры + examples_text = "" + if word_data.get("examples"): + examples_text = "\n\nПримеры:\n" + for idx, example in enumerate(word_data["examples"][:2], 1): + examples_text += f"{idx}. {example['en']}\n {example['ru']}\n" + + # Отправляем карточку слова + card_text = ( + f"📝 {word_data['word']}\n" + f"🔊 [{word_data.get('transcription', '')}]\n\n" + f"🇷🇺 {word_data['translation']}\n" + f"📂 Категория: {word_data.get('category', 'общая')}\n" + f"📊 Уровень: {word_data.get('difficulty', 'A1')}" + f"{examples_text}\n\n" + f"Добавить это слово в словарь?" + ) + + # Создаём inline-кнопки + keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [ + InlineKeyboardButton(text="✅ Добавить", callback_data=f"add_word_confirm"), + InlineKeyboardButton(text="❌ Отмена", 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): + """Подтверждение добавления слова""" + data = await state.get_data() + word_data = data.get("word_data") + user_id = data.get("user_id") + + async with async_session_maker() as session: + # Добавляем слово в базу + await VocabularyService.add_word( + session, + user_id=user_id, + word_original=word_data["word"], + word_translation=word_data["translation"], + 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) + + await callback.message.edit_text( + f"✅ Слово '{word_data['word']}' добавлено в твой словарь!\n\n" + f"Всего слов в словаре: {words_count}\n\n" + f"Продолжай добавлять новые слова или используй /task для практики!" + ) + + await state.clear() + await callback.answer() + + +@router.callback_query(F.data == "add_word_cancel") +async def cancel_add_word(callback: CallbackQuery, state: FSMContext): + """Отмена добавления слова""" + await callback.message.edit_text("Отменено. Можешь добавить другое слово командой /add") + await state.clear() + await callback.answer() + + +@router.message(Command("vocabulary")) +async def cmd_vocabulary(message: Message): + """Обработчик команды /vocabulary""" + 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("Сначала запусти бота командой /start") + return + + # Получаем слова пользователя + words = await VocabularyService.get_user_words(session, user.id, limit=10) + total_count = await VocabularyService.get_words_count(session, user.id) + + if not words: + await message.answer( + "📚 Твой словарь пока пуст!\n\n" + "Добавь первое слово командой /add или просто отправь мне слово." + ) + return + + # Формируем список слов + words_list = "📚 Твой словарь:\n\n" + for idx, word in enumerate(words, 1): + progress = "" + if word.times_reviewed > 0: + accuracy = int((word.correct_answers / word.times_reviewed) * 100) + progress = f" ({accuracy}% точность)" + + words_list += ( + f"{idx}. {word.word_original} — {word.word_translation}\n" + f" 🔊 [{word.transcription or ''}]{progress}\n\n" + ) + + if total_count > 10: + words_list += f"\nПоказаны последние 10 из {total_count} слов" + else: + words_list += f"\nВсего слов: {total_count}" + + await message.answer(words_list) diff --git a/bot/keyboards/__init__.py b/bot/keyboards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..315db6c --- /dev/null +++ b/config/settings.py @@ -0,0 +1,26 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + """Настройки приложения""" + + # Telegram + bot_token: str + + # OpenAI + openai_api_key: str + + # Database + database_url: str + + # App settings + debug: bool = False + + model_config = SettingsConfigDict( + env_file='.env', + env_file_encoding='utf-8', + case_sensitive=False + ) + + +settings = Settings() diff --git a/database/__init__.py b/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/database/db.py b/database/db.py new file mode 100644 index 0000000..658a7b4 --- /dev/null +++ b/database/db.py @@ -0,0 +1,29 @@ +from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession +from config.settings import settings +from database.models import Base + +# Создание движка базы данных +engine = create_async_engine( + settings.database_url, + echo=settings.debug, + future=True +) + +# Фабрика сессий +async_session_maker = async_sessionmaker( + engine, + class_=AsyncSession, + expire_on_commit=False +) + + +async def init_db(): + """Инициализация базы данных (создание таблиц)""" + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + +async def get_session() -> AsyncSession: + """Получение сессии базы данных""" + async with async_session_maker() as session: + yield session diff --git a/database/models.py b/database/models.py new file mode 100644 index 0000000..1b3f9a5 --- /dev/null +++ b/database/models.py @@ -0,0 +1,83 @@ +from datetime import datetime +from typing import Optional + +from sqlalchemy import String, BigInteger, DateTime, Integer, Boolean, JSON, Enum as SQLEnum +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column +import enum + + +class Base(DeclarativeBase): + """Базовая модель""" + pass + + +class LanguageLevel(str, enum.Enum): + """Уровни владения языком""" + A1 = "A1" + A2 = "A2" + B1 = "B1" + B2 = "B2" + C1 = "C1" + C2 = "C2" + + +class WordSource(str, enum.Enum): + """Источник добавления слова""" + MANUAL = "manual" # Ручное добавление + SUGGESTED = "suggested" # Предложено ботом + CONTEXT = "context" # Из контекста диалога + IMPORT = "import" # Импорт из текста + ERROR = "error" # Из ошибок в заданиях + + +class User(Base): + """Модель пользователя""" + __tablename__ = "users" + + id: Mapped[int] = mapped_column(primary_key=True) + telegram_id: Mapped[int] = mapped_column(BigInteger, unique=True, nullable=False) + username: Mapped[Optional[str]] = mapped_column(String(255)) + language_interface: Mapped[str] = mapped_column(String(2), default="ru") # ru/en + learning_language: Mapped[str] = mapped_column(String(2), default="en") # en + level: Mapped[LanguageLevel] = mapped_column(SQLEnum(LanguageLevel), default=LanguageLevel.A1) + timezone: Mapped[str] = mapped_column(String(50), default="UTC") + daily_task_time: Mapped[Optional[str]] = mapped_column(String(5)) # HH:MM + streak_days: Mapped[int] = mapped_column(Integer, default=0) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + last_active: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + +class Vocabulary(Base): + """Модель словарного запаса""" + __tablename__ = "vocabulary" + + id: Mapped[int] = mapped_column(primary_key=True) + user_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + word_original: Mapped[str] = mapped_column(String(255), nullable=False) + word_translation: Mapped[str] = mapped_column(String(255), nullable=False) + transcription: Mapped[Optional[str]] = mapped_column(String(255)) + examples: Mapped[Optional[dict]] = mapped_column(JSON) # JSON массив примеров + category: Mapped[Optional[str]] = mapped_column(String(100)) + difficulty_level: Mapped[Optional[LanguageLevel]] = mapped_column(SQLEnum(LanguageLevel)) + source: Mapped[WordSource] = mapped_column(SQLEnum(WordSource), default=WordSource.MANUAL) + times_reviewed: Mapped[int] = mapped_column(Integer, default=0) + correct_answers: Mapped[int] = mapped_column(Integer, default=0) + last_reviewed: Mapped[Optional[datetime]] = mapped_column(DateTime) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) + notes: Mapped[Optional[str]] = mapped_column(String(500)) # Заметки пользователя + + +class Task(Base): + """Модель задания""" + __tablename__ = "tasks" + + id: Mapped[int] = mapped_column(primary_key=True) + user_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + task_type: Mapped[str] = mapped_column(String(50), nullable=False) # translate, sentence, fill, etc. + content: Mapped[dict] = mapped_column(JSON, nullable=False) # Содержание задания + correct_answer: Mapped[Optional[str]] = mapped_column(String(500)) + user_answer: Mapped[Optional[str]] = mapped_column(String(500)) + is_correct: Mapped[Optional[bool]] = mapped_column(Boolean) + ai_feedback: Mapped[Optional[str]] = mapped_column(String(1000)) + completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) diff --git a/main.py b/main.py new file mode 100644 index 0000000..66580a8 --- /dev/null +++ b/main.py @@ -0,0 +1,41 @@ +import asyncio +import logging + +from aiogram import Bot, Dispatcher +from aiogram.client.default import DefaultBotProperties +from aiogram.enums import ParseMode + +from config.settings import settings +from bot.handlers import start, vocabulary +from database.db import init_db + + +async def main(): + """Главная функция запуска бота""" + # Настройка логирования + logging.basicConfig( + level=logging.INFO if settings.debug else logging.WARNING, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Инициализация бота и диспетчера + bot = Bot( + token=settings.bot_token, + default=DefaultBotProperties(parse_mode=ParseMode.HTML) + ) + dp = Dispatcher() + + # Регистрация роутеров + dp.include_router(start.router) + dp.include_router(vocabulary.router) + + # Инициализация базы данных + await init_db() + + # Запуск бота + logging.info("Бот запущен") + await dp.start_polling(bot) + + +if __name__ == '__main__': + asyncio.run(main()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..bf6481e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +aiogram==3.13.1 +sqlalchemy==2.0.36 +asyncpg==0.30.0 +alembic==1.14.0 +python-dotenv==1.0.1 +openai==1.57.3 +pydantic==2.10.3 +pydantic-settings==2.6.1 diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/ai_service.py b/services/ai_service.py new file mode 100644 index 0000000..19ffdc3 --- /dev/null +++ b/services/ai_service.py @@ -0,0 +1,117 @@ +from openai import AsyncOpenAI +from config.settings import settings +from typing import Dict, List + + +class AIService: + """Сервис для работы с OpenAI API""" + + def __init__(self): + self.client = AsyncOpenAI(api_key=settings.openai_api_key) + + async def translate_word(self, word: str, target_lang: str = "ru") -> Dict: + """ + Перевести слово и получить дополнительную информацию + + Args: + word: Слово для перевода + target_lang: Язык перевода (по умолчанию русский) + + Returns: + Dict с переводом, транскрипцией и примерами + """ + prompt = f"""Переведи английское слово/фразу "{word}" на русский язык. + +Верни ответ строго в формате JSON: +{{ + "word": "{word}", + "translation": "перевод", + "transcription": "транскрипция в IPA", + "examples": [ + {{"en": "пример на английском", "ru": "перевод примера"}}, + {{"en": "ещё один пример", "ru": "перевод примера"}} + ], + "category": "категория слова (работа, еда, путешествия и т.д.)", + "difficulty": "уровень сложности (A1/A2/B1/B2/C1/C2)" +}} + +Важно: верни только JSON, без дополнительного текста.""" + + try: + response = await self.client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + {"role": "system", "content": "Ты - помощник для изучения английского языка. Отвечай только в формате JSON."}, + {"role": "user", "content": prompt} + ], + temperature=0.3, + response_format={"type": "json_object"} + ) + + import json + result = json.loads(response.choices[0].message.content) + return result + + except Exception as e: + # Fallback в случае ошибки + return { + "word": word, + "translation": "Ошибка перевода", + "transcription": "", + "examples": [], + "category": "unknown", + "difficulty": "A1" + } + + async def check_answer(self, question: str, correct_answer: str, user_answer: str) -> Dict: + """ + Проверить ответ пользователя с помощью ИИ + + Args: + question: Вопрос задания + correct_answer: Правильный ответ + user_answer: Ответ пользователя + + Returns: + Dict с результатом проверки и обратной связью + """ + prompt = f"""Проверь ответ пользователя на задание по английскому языку. + +Задание: {question} +Правильный ответ: {correct_answer} +Ответ пользователя: {user_answer} + +Верни ответ в формате JSON: +{{ + "is_correct": true/false, + "feedback": "краткое объяснение (если ответ неверный, объясни ошибку и дай правильный вариант)", + "score": 0-100 +}} + +Учитывай возможные вариации ответа. Если смысл передан правильно, даже с небольшими грамматическими неточностями, засчитывай ответ.""" + + try: + response = await self.client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + {"role": "system", "content": "Ты - преподаватель английского языка. Проверяй ответы справедливо, учитывая контекст."}, + {"role": "user", "content": prompt} + ], + temperature=0.3, + response_format={"type": "json_object"} + ) + + import json + result = json.loads(response.choices[0].message.content) + return result + + except Exception as e: + return { + "is_correct": False, + "feedback": "Ошибка проверки ответа", + "score": 0 + } + + +# Глобальный экземпляр сервиса +ai_service = AIService() diff --git a/services/user_service.py b/services/user_service.py new file mode 100644 index 0000000..7f08fe9 --- /dev/null +++ b/services/user_service.py @@ -0,0 +1,59 @@ +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from database.models import User, LanguageLevel +from typing import Optional + + +class UserService: + """Сервис для работы с пользователями""" + + @staticmethod + async def get_or_create_user(session: AsyncSession, telegram_id: int, username: Optional[str] = None) -> User: + """ + Получить пользователя или создать нового + + Args: + session: Сессия базы данных + telegram_id: Telegram ID пользователя + username: Username пользователя + + Returns: + Объект пользователя + """ + # Попытка найти существующего пользователя + result = await session.execute( + select(User).where(User.telegram_id == telegram_id) + ) + user = result.scalar_one_or_none() + + if user: + return user + + # Создание нового пользователя + new_user = User( + telegram_id=telegram_id, + username=username, + level=LanguageLevel.A1 + ) + session.add(new_user) + await session.commit() + await session.refresh(new_user) + + return new_user + + @staticmethod + async def get_user_by_telegram_id(session: AsyncSession, telegram_id: int) -> Optional[User]: + """ + Получить пользователя по Telegram ID + + Args: + session: Сессия базы данных + telegram_id: Telegram ID пользователя + + Returns: + Объект пользователя или None + """ + result = await session.execute( + select(User).where(User.telegram_id == telegram_id) + ) + return result.scalar_one_or_none() diff --git a/services/vocabulary_service.py b/services/vocabulary_service.py new file mode 100644 index 0000000..2ecd651 --- /dev/null +++ b/services/vocabulary_service.py @@ -0,0 +1,123 @@ +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from database.models import Vocabulary, WordSource, LanguageLevel +from typing import List, Optional + + +class VocabularyService: + """Сервис для работы со словарным запасом""" + + @staticmethod + async def add_word( + session: AsyncSession, + user_id: int, + word_original: str, + word_translation: str, + transcription: Optional[str] = None, + examples: Optional[dict] = None, + category: Optional[str] = None, + difficulty_level: Optional[str] = None, + source: WordSource = WordSource.MANUAL, + notes: Optional[str] = None + ) -> Vocabulary: + """ + Добавить слово в словарь пользователя + + Args: + session: Сессия базы данных + user_id: ID пользователя + word_original: Оригинальное слово + word_translation: Перевод + transcription: Транскрипция + examples: Примеры использования + category: Категория слова + difficulty_level: Уровень сложности + source: Источник добавления + notes: Заметки пользователя + + Returns: + Созданный объект слова + """ + # Преобразование difficulty_level в enum + difficulty_enum = None + if difficulty_level: + try: + difficulty_enum = LanguageLevel[difficulty_level] + except KeyError: + difficulty_enum = None + + new_word = Vocabulary( + user_id=user_id, + word_original=word_original, + word_translation=word_translation, + transcription=transcription, + examples=examples, + category=category, + difficulty_level=difficulty_enum, + source=source, + notes=notes + ) + + session.add(new_word) + await session.commit() + await session.refresh(new_word) + + return new_word + + @staticmethod + async def get_user_words(session: AsyncSession, user_id: int, limit: int = 50) -> List[Vocabulary]: + """ + Получить все слова пользователя + + Args: + session: Сессия базы данных + user_id: ID пользователя + limit: Максимальное количество слов + + Returns: + Список слов пользователя + """ + result = await session.execute( + select(Vocabulary) + .where(Vocabulary.user_id == user_id) + .order_by(Vocabulary.created_at.desc()) + .limit(limit) + ) + return list(result.scalars().all()) + + @staticmethod + async def get_words_count(session: AsyncSession, user_id: int) -> int: + """ + Получить количество слов в словаре пользователя + + Args: + session: Сессия базы данных + user_id: ID пользователя + + Returns: + Количество слов + """ + result = await session.execute( + select(Vocabulary).where(Vocabulary.user_id == user_id) + ) + return len(list(result.scalars().all())) + + @staticmethod + async def find_word(session: AsyncSession, user_id: int, word: str) -> Optional[Vocabulary]: + """ + Найти слово в словаре пользователя + + Args: + session: Сессия базы данных + user_id: ID пользователя + word: Слово для поиска + + Returns: + Объект слова или None + """ + result = await session.execute( + select(Vocabulary) + .where(Vocabulary.user_id == user_id) + .where(Vocabulary.word_original.ilike(f"%{word}%")) + ) + return result.scalar_one_or_none()