### Environment Configuration Variables (.env) Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt This outlines the structure of the `.env` file used for environment configuration, including settings for the Telegram bot token, admin ID, database credentials, web converter login and password, logging level, and audio processing parameters like max file size and default bitrate. It shows an example `.env.example` file structure. ```bash # .env file structure (copy from .env.example): # Telegram Bot BOT_TOKEN=1234567890:ABCdefGHIjklMNOpqrSTUvwxYZ ADMIN_TELEGRAM_ID=123456789 # Database DB_NAME=audio_bot DB_USER=postgres DB_PASSWORD=secure_password DB_HOST=db DB_PORT=5432 # Web Converter WEB_CONVERTER_LOGIN=admin WEB_CONVERTER_PASSWORD=secure_converter_pass WEB_CONVERTER_SECRET=random_secret_key # Logging LOG_LEVEL=INFO # Audio Settings MAX_FILE_SIZE_MB=50 DEFAULT_BITRATE_KBPS=64 ``` -------------------------------- ### Docker Compose Deployment and Management (YAML/Bash) Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt This section details Docker Compose configurations for deploying and managing the Telegram bot and its associated services (database, adminer, web converter). It includes commands for starting, viewing logs, restarting, executing commands within containers, performing database migrations, accessing PostgreSQL, stopping, and resetting services. It assumes a `docker-compose.yml` file is present. ```yaml # docker-compose.yml structure: services: bot: # Main Telegram bot (aiogram 3.x) db: # PostgreSQL 15 with Moscow timezone adminer: # Database admin UI (port 8080) web-converter: # Audio converter service (port 1992) # Start all services: docker-compose up -d --build # View bot logs: docker-compose logs bot --tail=50 --follow # Restart bot after code changes: docker-compose restart bot # Execute commands in bot container: docker-compose exec bot python init_data.py # Database migrations: docker-compose exec bot alembic revision --autogenerate -m "Add field" docker-compose exec bot alembic upgrade head # Make user admin: docker-compose exec db psql -U postgres -d audio_bot -c \ "UPDATE users SET role_id = 1 WHERE telegram_id = 123456789;" # Access PostgreSQL: docker-compose exec db psql -U postgres -d audio_bot # Stop all services: docker-compose down # Reset database (WARNING: destroys all data): docker-compose down -v docker-compose up -d ``` -------------------------------- ### Lesson Management: Create, Update, Delete, and Get Lessons (Python) Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt Demonstrates the core CRUD operations for lessons in the tg-islamic-bot. This includes creating new lessons linked to series, updating existing lesson details, deleting lessons, and fetching lessons with detailed filtering and eager loading of related objects like series, teachers, and books. It utilizes SQLAlchemy for asynchronous database interactions. ```python new_lesson = await create_lesson( series_id=45, # Required - links to lesson_series book_id=12, teacher_id=3, theme_id=1, title="Введение в науку хадис", lesson_number=1, description="Определение хадиса и его важность", audio_path="bot/audio_files/teacher_name/book_name/2024_series/урок_1_введение.mp3", duration_seconds=3600, # 1 hour tags="хадис, мусталах, введение" ) # Update lesson (modify object then call update) lesson.title = "Новое название" lesson.description = "Обновленное описание" lesson.tags = "новый_тег, другой_тег" await update_lesson(lesson) # CRITICAL: Reload lesson after update to get fresh relationships lesson = await get_lesson_by_id(lesson.id) # Delete lesson (cascades to bookmarks, test_questions; sets test_attempts.lesson_id to NULL) success = await delete_lesson(lesson_id=100) # Get all lessons with filters from bot.models import Lesson, async_session_maker from sqlalchemy import select from sqlalchemy.orm import joinedload async with async_session_maker() as session: result = await session.execute( select(Lesson) .options( joinedload(Lesson.series), joinedload(Lesson.teacher), joinedload(Lesson.book).joinedload(Book.author) ) .where(Lesson.is_active == True) .where(Lesson.series_id == 45) .order_by(Lesson.lesson_number) ) lessons = result.scalars().unique().all() ``` -------------------------------- ### User and Role Management (Python) Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt Handles user authentication and role-based access control. This snippet shows how to get or create users based on Telegram ID, retrieve user roles, and update user roles. It interacts with the database service to manage user data and their associated roles. ```python from bot.services.database_service import ( get_user_by_telegram_id, get_or_create_user, update_user_role, get_all_users ) # Get or create user from Telegram data user = await get_or_create_user( telegram_id=123456789, username="john_doe", first_name="John", last_name="Doe" ) # If user exists: returns existing user # If new: creates with role_id=3 (User role) # Check user role (relationship pre-loaded) print(f"Role: {user.role.name}") # "admin", "moderator", or "user" print(f"Is admin: {user.role.name == 'admin'}") # Update user role await update_user_role(telegram_id=123456789, role_id=1) # Make admin await update_user_role(telegram_id=123456789, role_id=2) # Make moderator await update_user_role(telegram_id=123456789, role_id=3) # Make user # List all users (paginated) users = await get_all_users(limit=50, offset=0) for user in users: print(f"{user.telegram_id} ({user.username}): {user.role.name}") ``` -------------------------------- ### Get Feedback Counts by Status (Python) Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt This Python code snippet retrieves the counts of feedbacks based on their status (new, replied, closed) using an asynchronous function `get_feedbacks_by_status`. It then prints a formatted summary of these counts. It assumes the existence of the `get_feedbacks_by_status` function and a `print` statement for output. ```python new_count = len(await get_feedbacks_by_status("new")) replied_count = len(await get_feedbacks_by_status("replied")) closed_count = len(await get_feedbacks_by_status("closed")) print(f"Feedback stats: 🆕 {new_count} | ✅ {replied_count} | 🔒 {closed_count}") ``` -------------------------------- ### Debug Handler Triggering with Regex Filter Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md Example of how to define a handler filter using regular expressions in Python with aiogram. This is useful for matching specific patterns in callback data, ensuring the correct handler is triggered. ```python from aiogram.filters import F # Example filter for callback data starting with 'edit_lesson_' followed by digits # This helps in debugging handler order and filter syntax issues. # F.data.regexp(r"^edit_lesson_\d+$") ``` -------------------------------- ### Manage Test and Question Data Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt Facilitates the management of tests and questions within the bot. It includes functions to retrieve test details based on a series ID, get questions associated with a lesson, create new tests and questions, submit test answers, and retrieve a user's test attempts. This functionality relies on the database service. ```python from bot.services.database_service import ( get_test_by_series, get_questions_by_lesson, create_test, create_test_question, submit_test_answer, get_user_test_attempts ) # Get test for series test = await get_test_by_series(series_id=45) if test: print(f"{test.display_title}") # "Экзамен (2024 - Основы Акыды)" print(f"Questions: {test.questions_count}") print(f"Passing score: {test.passing_score}% ({test.passing_points}/{test.max_score})") print(f"Time: {test.formatted_time}") # "15 мин 0 сек" print(f"Full info:\n{test.full_info}") ``` -------------------------------- ### Moscow Time Operations (Python) Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt This Python code demonstrates how to handle Moscow time (UTC+3) using `bot.utils.timezone_utils`. It shows how to get the current Moscow time as a naive datetime object suitable for database storage and how to format it for display. It emphasizes the importance of using naive datetimes for database operations to avoid issues. ```python from bot.utils.timezone_utils import get_moscow_now, MOSCOW_TZ from datetime import datetime # Get current Moscow time (naive datetime for DB) now = get_moscow_now() print(now) # datetime.datetime(2024, 1, 15, 14, 30, 0) print(now.tzinfo) # None (naive) # Store in database (all timestamps use get_moscow_now as default) from bot.models import Lesson lesson = Lesson( title="New lesson", created_at=get_moscow_now(), # Automatically set if not provided updated_at=get_moscow_now() # Auto-updated on changes ) # IMPORTANT: Never use timezone-aware datetimes # ❌ WRONG: # aware_time = datetime.now(MOSCOW_TZ) # lesson.created_at = aware_time # Will cause issues # ✅ CORRECT: # naive_time = get_moscow_now() # lesson.created_at = naive_time # Display formatted timestamps print(f"Created: {lesson.created_at.strftime('%d.%m.%Y %H:%M:%S')}") # "Created: 15.01.2024 14:30:00" ``` -------------------------------- ### Project Startup and Initialization Commands Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/AGENTS.md These bash commands are used to build, run, and initialize the TG Islamic Bot project. They cover full deployment, initial data seeding, and assigning administrator roles. ```bash # Полный запуск с инициализацией docker-compose up -d --build # Инициализация начальных данных (только после первого запуска) docker-compose exec bot python init_data.py # Назначение прав администратора docker-compose exec db psql -U postgres -d audio_bot -c "UPDATE users SET role_id = 1 WHERE telegram_id = ВАШ_TELEGRAM_ID;" ``` -------------------------------- ### Database Migration Commands with Alembic Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md This Bash snippet outlines the essential commands for managing database migrations using Alembic within a Dockerized environment. It covers creating new migrations with autogeneration, reviewing the generated scripts, and applying them to the database. It also includes notes on best practices for migration management. ```bash # Create migration docker-compose exec bot alembic revision --autogenerate -m "Add field to Lesson" # Review in alembic/versions/ (autogenerate isn't perfect) # Apply docker-compose exec bot alembic upgrade head ``` -------------------------------- ### Python: Series Model and Lesson Retrieval Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt Illustrates how to fetch and display information about a series, including its metadata, lesson statistics, and a list of its associated lessons. The code demonstrates accessing properties like display name, teacher, book title, theme, completion status, and various lesson counts, as well as iterating through lessons within a series. ```python from bot.models import LessonSeries from bot.services.database_service import get_series_by_id, get_lessons_by_series # Fetch series with relationships series = await get_series_by_id(series_id=45) # Series metadata print(f"Display: {series.display_name}") # "2024 - Основы Акыды" print(f"Teacher: {series.teacher_name}") # "Мухаммад Абу Мунира" print(f"Book: {series.book_title}") # "4 правила" or None print(f"Theme: {series.theme_name}") # "Акыда" (from book or direct) print(f"Completed: {series.is_completed}") # True/False # Lesson statistics print(f"Total lessons: {series.total_lessons}") # 25 print(f"Active lessons: {series.active_lessons_count}") # 23 print(f"Total duration: {series.formatted_total_duration}") # "15в 30м" # Fetch all lessons in series (sorted by lesson_number) lesssons = await get_lessons_by_series(series_id=45) for lesson in lessons: print(f"Lesson {lesson.lesson_number}: {lesson.title}") # Full info display (for admin/user views) info_text = series.full_info # Output: # 📚 2024 - Основы Акыды # 👤 Мухаммад Абу Мунира # 📚 Книга: 4 правила # 📋 Тема: Акыда # 🎧 Уроков: 23/25 # 🕒 Длительность: 15в 30м # ✅ Серия завершена ``` -------------------------------- ### Audio Conversion and Analysis Utilities (Python) Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt Provides a suite of tools for managing audio files, including conversion to MP3 with automatic bitrate detection, calculating audio duration, retrieving detailed audio information, and determining the optimal bitrate for a target file size. These utilities are crucial for processing and storing lesson audio content efficiently. ```python from bot.utils.audio_converter import ( convert_to_mp3_auto, get_audio_duration, get_audio_info, calculate_optimal_bitrate ) # Convert audio file with automatic bitrate selection # Tries 64 kbps first, calculates optimal if file > 50 MB success, error, used_bitrate = await convert_to_mp3_auto( input_path="/tmp/uploaded_lesson.wav", output_path="bot/audio_files/teacher/book/series/урок_1.mp3", preferred_bitrate=64, # kbps channels=1, # mono sample_rate=44100, normalize=True, # Loudness normalization max_attempts=2 ) if success: print(f"Converted at {used_bitrate} kbps") else: print(f"Error: {error}") # Get audio duration duration_seconds = await get_audio_duration("path/to/audio.mp3") print(f"Duration: {duration_seconds} seconds") # 3600 # Get full audio info info = await get_audio_info("path/to/audio.mp3") print(info) # { # 'duration': 3600, # 'format': 'mp3', # 'bitrate': 64000, # 'sample_rate': 44100, # 'channels': 1, # 'size': 28800000 # } # Calculate optimal bitrate for target size optimal = await calculate_optimal_bitrate( duration_seconds=7200, # 2 hours target_size_mb=49 # Just under 50 MB limit ) print(f"Optimal bitrate: {optimal} kbps") # 28 kbps (calculated) ``` -------------------------------- ### Python: Manage Lesson Questions and Test Creation Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt This snippet demonstrates how to retrieve questions for a specific lesson, create a new test for a series, add questions to a test, and simulate a user taking and submitting a test. It uses asynchronous functions and database models for test and attempt management. ```python questions = await get_questions_by_lesson(test_id=test.id, lesson_id=100) for q in questions: print(f"Q{q.order}: {q.question_text}") print(f" Options: {q.options_list}") # ['A', 'B', 'C', 'D'] print(f" Correct: {q.correct_answer}") # 'B' new_test = await create_test( series_id=45, teacher_id=3, title="Экзамен по серии", description="Тест на знание основ акыды", passing_score=80, # 80% to pass time_per_question_seconds=30 ) question = await create_test_question( test_id=new_test.id, lesson_id=100, # Optional: link to specific lesson question_text="Что такое таухид?", options="Единобожие;Многобожие;Неверие;Сомнение", # Semicolon-separated correct_answer="Единобожие", explanation="Таухид - это единобожие, основа ислама", order=1 ) from bot.models.test_attempt import TestAttempt attempt = TestAttempt( user_id=42, test_id=new_test.id, lesson_id=100 # Optional ) await session.add(attempt) await session.commit() score = await submit_test_answer( attempt_id=attempt.id, question_id=question.id, user_answer="Единобожие" ) attempts = await get_user_test_attempts(user_id=42, test_id=new_test.id) for attempt in attempts: print(f"Score: {attempt.score}/{attempt.test.max_score} ({attempt.percentage}%)") print(f"Passed: {attempt.passed}") print(f"Date: {attempt.completed_at.strftime('%d.%m.%Y %H:%M')}") ``` -------------------------------- ### Handle User Lesson Playback with Audio Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt Handles user requests to play lessons, including audio playback, displaying lesson details, checking for associated tests and bookmarks, and managing audio file caching. It uses aiogram and a database service to retrieve lesson information and user data. ```python from aiogram import Router, F from aiogram.types import CallbackQuery, FSInputFile from bot.services.database_service import ( get_lesson_by_id, get_test_by_series, get_questions_by_lesson, get_bookmark_by_user_and_lesson, get_user_by_telegram_id, update_lesson ) from bot.keyboards.user import get_lesson_control_keyboard from bot.utils.decorators import user_required_callback from bot.utils.audio_utils import AudioUtils router = Router() @router.callback_query(F.data.startswith("lesson_")) @user_required_callback async def play_lesson(callback: CallbackQuery): """Play lesson with context-aware navigation""" lesson_id = int(callback.data.split("_")[1]) lesson = await get_lesson_by_id(lesson_id) if not lesson or not lesson.has_audio(): await callback.answer("Аудиофайл недоступен", show_alert=True) return # Build caption with lesson info caption = ( f"🎧 Урок {lesson.lesson_number}\n\n" f"📖 Книга: «{lesson.book_title}»\n" f"✍️ Автор: {lesson.book.author_info if lesson.book else 'Не указан'}\n" f"🎙️ Преподаватель: {lesson.teacher_name}\n" f"⏱️ Длительность: {lesson.formatted_duration}\n" ) if lesson.tags_list: caption += f"🏷️ Теги: {', '.join(lesson.tags_list)}\n" if lesson.description: caption += f"\n📝 {lesson.description}" # Check for test availability has_test = False if lesson.series_id: test = await get_test_by_series(lesson.series_id) if test and test.is_active: questions = await get_questions_by_lesson(test.id, lesson.id) has_test = len(questions) > 0 # Check for bookmark user = await get_user_by_telegram_id(callback.from_user.id) bookmark = await get_bookmark_by_user_and_lesson(user.id, lesson_id) if user else None has_bookmark = bookmark is not None # Build keyboard (Previous/Next, Test, Bookmark, Back) keyboard = get_lesson_control_keyboard(lesson, has_test=has_test, has_bookmark=has_bookmark) # Delete previous message (single-window pattern) try: await callback.message.delete() except: pass # Send audio (with file_id caching) if lesson.telegram_file_id: # Fast path: cached file_id await callback.message.answer_audio( audio=lesson.telegram_file_id, caption=caption, reply_markup=keyboard ) else: # Slow path: first upload audio_file = FSInputFile(lesson.audio_path) sent = await callback.message.answer_audio( audio=audio_file, title=lesson.title, caption=caption, reply_markup=keyboard ) # Cache file_id if sent.audio: lesson.telegram_file_id = sent.audio.file_id await update_lesson(lesson) await callback.answer() ``` -------------------------------- ### Python: Feedback System Workflow Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt This snippet outlines a three-status feedback workflow using asynchronous functions from `database_service`. It covers creating feedback, retrieving feedbacks by status, replying to feedback, and closing feedback. The workflow transitions feedback from 'new' to 'replied' and finally to 'closed'. ```python from bot.services.database_service import ( create_feedback, get_feedback_by_id, get_feedbacks_by_status, update_feedback_status, reply_to_feedback, close_feedback ) # User submits feedback feedback = await create_feedback( user_id=42, message_text="Не могу найти урок по намазу" ) # Status: "new" # Admin views new feedbacks new_feedbacks = await get_feedbacks_by_status(status="new") for fb in new_feedbacks: print(f"{fb.status_emoji} [{fb.created_at.strftime('%d.%m %H:%M')}]") print(f"User: {fb.user.first_name} (@{fb.user.username})") print(f"Message: {fb.message_text}\n") # Admin replies to user await reply_to_feedback( feedback_id=feedback.id, admin_reply="Уроки по намазу находятся в разделе Фикх → Намаз", bot=bot # Bot instance to send message to user ) # Status: "new" → "replied" # replied_at: set to current Moscow time # User receives reply via bot message # Admin closes feedback await close_feedback(feedback_id=feedback.id) # Status: "replied" → "closed" # closed_at: set to current Moscow time ``` -------------------------------- ### Python: Database Service CRUD Operations Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt Demonstrates essential Create, Read, Update, and Delete (CRUD) operations for lessons using the `database_service` layer. It specifically shows how to fetch a lesson with all its related objects (series, teacher, book, theme) eagerly loaded to prevent common ORM errors like `DetachedInstanceError`. ```python from bot.services.database_service import ( get_lesson_by_id, create_lesson, update_lesson, delete_lesson, get_all_lessons ) # Get lesson with all relationships loaded (prevents DetachedInstanceError) lessson = await get_lesson_by_id(lesson_id=100) # Relationships loaded: series, teacher, book, book.author, book.theme, theme ``` -------------------------------- ### Python: FastAPI Audio Conversion API with aiohttp Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt This snippet defines asynchronous functions to interact with a FastAPI web converter service for audio file conversion. It includes functionalities for uploading and converting files, and downloading the converted output. Dependencies include `aiohttp`. ```python import aiohttp async def upload_and_convert(file_path: str, bitrate: str = "64") -> dict: """Upload file to web converter service""" url = "http://web-converter:1992/convert" async with aiohttp.ClientSession() as session: with open(file_path, 'rb') as f: form = aiohttp.FormData() form.add_field('file', f, filename='lesson.wav') form.add_field('bitrate', bitrate) async with session.post(url, data=form) as resp: if resp.status == 200: result = await resp.json() # { # "filename": "lesson_1234567890.mp3", # "duration": "1ч 30м 0с", # "bitrate": 64, # "mp3_size": "43.2 МБ", # "original_size": "152.0 МБ" # } return result else: error = await resp.json() raise Exception(error['detail']) # Download converted file async def download_converted(filename: str, save_path: str): """Download converted file from web converter""" url = f"http://web-converter:1992/download/{filename}" async with aiohttp.ClientSession() as session: async with session.get(url) as resp: if resp.status == 200: with open(save_path, 'wb') as f: f.write(await resp.read()) else: raise Exception("File not found") # Bitrate options: # - "64" = 64 kbps (up to 40 min in 50 MB) # - "48" = 48 kbps (up to 1 hour in 50 MB) # - "32" = 32 kbps (up to 1.5 hours in 50 MB) # - "auto" = automatically calculate optimal bitrate # Web interface available at: http://localhost:1992 # - Login with credentials from .env (WEB_CONVERTER_LOGIN, WEB_CONVERTER_PASSWORD) # - Drag & drop audio files up to 2 GB # - PWA-enabled for mobile installation ``` -------------------------------- ### Manage User Bookmarks Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt Provides functionality to manage user bookmarks, including retrieving all bookmarks for a user, checking for existing bookmarks for a specific lesson, counting bookmarks to enforce a limit, creating new bookmarks, renaming them, and deleting them. It relies on a database service for these operations. ```python from bot.services.database_service import ( get_bookmarks_by_user, get_bookmark_by_user_and_lesson, count_user_bookmarks, create_bookmark, update_bookmark_name, delete_bookmark ) # Get user's bookmarks (ordered by created_at desc) user_id = 42 bookmarks = await get_bookmarks_by_user(user_id) print(f"User has {len(bookmarks)} bookmarks") for bookmark in bookmarks: print(f"{bookmark.custom_name} | {bookmark.created_at.strftime('%d.%m.%Y')}") print(f" Lesson: {bookmark.lesson.title}") # Check if bookmark exists existing = await get_bookmark_by_user_and_lesson(user_id=42, lesson_id=100) if existing: print(f"Already bookmarked as: {existing.custom_name}") # Count bookmarks (enforces 20 limit) count = await count_user_bookmarks(user_id=42) if count >= 20: print("❌ Bookmark limit reached (20 max)") else: # Create new bookmark bookmark = await create_bookmark( user_id=42, lesson_id=100, custom_name="Важный урок о таухиде" ) print(f"✅ Bookmark created: {bookmark.id}") # Rename bookmark await update_bookmark_name( bookmark_id=bookmark.id, new_name="Переименованная закладка" ) # Delete bookmark await delete_bookmark(bookmark_id=bookmark.id) ``` -------------------------------- ### Python: Lesson Model and Data Access Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt Demonstrates fetching lesson data with its related series, teacher, and book information using the `get_lesson_by_id` service function. It highlights how to correctly access related fields through relationships, avoiding issues with detached instances, and displays various lesson properties like title, number, duration, and tags. ```python from bot.models import Lesson from bot.services.database_service import get_lesson_by_id # Fetch lesson with eager-loaded relationships to avoid DetachedInstanceError lesson = await get_lesson_by_id(lesson_id=123) # Access series information through relationship (NOT through removed fields) # ☠ WRONG - these fields were removed in migration: # year = lesson.series_year # name = lesson.series_name # ✅ CORRECT - use relationship: if lesson.series: series_year = lesson.series.year # e.g., 2024 series_name = lesson.series.name # e.g., "Основы Акыды" series_display = lesson.series.display_name # "2024 - Основы Акыды" teacher_name = lesson.series.teacher.name # Through series relationship # Lesson properties print(f"Title: {lesson.title}") print(f"Number: {lesson.lesson_number}") print(f"Duration: {lesson.formatted_duration}") # "1в 23м 45с" print(f"Display: {lesson.display_title}") # "Урок 5" print(f"Full: {lesson.full_display_title}") # "2024 - Основы Акыды | Урок 5" print(f"Has audio: {lesson.has_audio()}") # True/False print(f"Tags: {lesson.tags_list}") # ['акыда', 'таухид'] ``` -------------------------------- ### Python: Theme and Book Hierarchy Operations Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt Shows how to retrieve themes and books, demonstrating the hierarchical structure of content. It covers fetching all active themes, a specific theme with its associated books, and books filtered by theme (including orphaned books). The code also accesses details like theme names, book display names, author information, and lesson counts. ```python from bot.services.database_service import ( get_all_active_themes, get_theme_by_id, get_books_by_theme, get_book_by_id ) # Fetch active themes (sorted by sort_order) themes = await get_all_active_themes() for theme in themes: print(f"{theme.name}: {theme.active_books_count} books") # "Акыда: 12 books" # "Сира: 8 books" # Fetch theme with books eagerly loaded theme = await get_theme_by_id(theme_id=1) print(f"Theme: {theme.name}") print(f"Description: {theme.desc}") active_books = [b for b in theme.books if b.is_active] # Fetch books by theme (with authors eagerly loaded) books = await get_books_by_theme(theme_id=1) for book in books: print(f"{book.display_name}") # "«4 правила» - Мухаммад ибн Абдуль-Ваххаб" print(f"Author info: {book.author_info}") # "Мухаммад ибн Абдуль-Ваххаб (1703-1792)" print(f"Lessons: {book.active_lessons_count}") # Books without theme (orphaned books) orphaned_books = await get_books_by_theme(theme_id=None) ``` -------------------------------- ### Grouping Lessons by Series with SQLAlchemy Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md This Python snippet utilizes SQLAlchemy to fetch lessons and their associated series, then groups them into a dictionary keyed by series ID. It demonstrates fetching data with `joinedload` for efficient relationship loading and then programmatically organizing the results, which is useful for displaying series-based content. ```python # Load lessons with series relationship result = await session.execute( select(Lesson) .options(joinedload(Lesson.series)) .where(Lesson.teacher_id == teacher_id) .order_by(Lesson.series_id) ) lessons = result.scalars().unique().all() # Group by series series_map = {} for lesson in lessons: if lesson.series_id: key = f"series_{lesson.series_id}" if key not in series_map: series_map[key] = { "series_id": lesson.series_id, "year": lesson.series.year, # From relationship "name": lesson.series.name, # From relationship "lessons": [] } series_map[key]["lessons"].append(lesson) ``` -------------------------------- ### Lesson Search and Filtering (Python) Source: https://context7.com/muwahhidun/tg-islamic-bot/llms.txt Enables searching for lessons based on keywords found in their title, description, or tags. This function efficiently filters lessons, ensuring results are from active books and associated with active authors. The search is case-insensitive and uses pattern matching for flexibility. ```python from bot.services.database_service import search_lessons # Search lessons by title, description, or tags # Only returns lessons from active books with active authors (or no author) results = await search_lessons(query="хадис") for lesson in results: print(f"{lesson.title}") print(f"Book: {lesson.book_title}") print(f"Teacher: {lesson.teacher_name}") print(f"Tags: {lesson.tags_list}") # Search is case-insensitive and uses LIKE pattern matching # Query "Хадис" finds: "Введение в хадис", "хадисоведение", etc. ``` -------------------------------- ### Correct Series Sorting in SQL Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md This SQL code snippet shows the correct way to order lessons by series, specifically addressing an issue where the previous method was broken. It highlights the importance of ordering by `series_id` and `lesson_number` for accurate series-based sorting, contrasting it with the incorrect approach. ```sql # ❌ OLD (broken) # .order_by(Lesson.series_year.desc(), Lesson.series_name) # ✅ NEW (correct) .order_by(Lesson.series_id, Lesson.lesson_number) ``` -------------------------------- ### View Docker Compose Logs for Bot Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md Commands to view logs for the bot service managed by docker-compose. Supports real-time following, limiting output to the last N lines, and searching for specific patterns like 'ERROR'. ```bash # Real-time docker-compose logs bot --follow # Last N lines docker-compose logs bot --tail=50 # Search docker-compose logs bot | grep ERROR ``` -------------------------------- ### Python: Edit Theme Name Handler Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md Handles the callback query to initiate the theme name editing process. It extracts the theme ID, stores message coordinates for later updates, sets the FSM state to 'name', and prompts the user for input. ```python from aiogram import F from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery # Assuming 'router' and 'ThemeStates' are defined elsewhere @router.callback_query(F.data.startswith("edit_theme_name_")) async def edit_theme_name_start(callback: CallbackQuery, state: FSMContext): theme_id = int(callback.data.split("_")[3]) # Save message coordinates for later update await state.update_data( theme_id=theme_id, edit_message_id=callback.message.message_id, edit_chat_id=callback.message.chat.id ) await state.set_state(ThemeStates.name) # ... prompt for input ``` -------------------------------- ### Context-Aware Callbacks for Navigation Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md This Python code illustrates how to retrieve saved context from an FSM state and construct dynamic callback data for Telegram bot navigation. It allows the bot to determine the user's previous navigation point (e.g., teacher view vs. general view) and generate an appropriate callback, facilitating seamless user experience and context preservation. ```python # Pattern: Context-aware callbacks data = await state.get_data() teacher_id = data.get("teacher_id") if teacher_id: # User came from teacher view - return there callback_data = f"teacher_{teacher_id}_play_lesson_{lesson_id}" else: # User came from general view callback_data = f"lesson_{lesson_id}" ``` -------------------------------- ### Python: Eager Loading with SQLAlchemy Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md Demonstrates how to perform eager loading of related entities (like 'series', 'teacher', 'book') when querying a 'Lesson' object using SQLAlchemy's `joinedload`. This pattern helps prevent N+1 query issues and DetachedInstanceError by loading all necessary data in a single query. ```python from sqlalchemy.orm import joinedload from sqlalchemy.future import select # Assuming 'session', 'Lesson', 'lesson_id' are defined elsewhere result = await session.execute( select(Lesson) .options( joinedload(Lesson.series), # Load series relationship joinedload(Lesson.teacher), joinedload(Lesson.book) ) .where(Lesson.id == lesson_id) ) lesson = result.scalar_one_or_none() ``` -------------------------------- ### Python: Save Theme Name Handler Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md Handles saving the theme name, differentiating between editing an existing theme and creating a new one. For editing, it updates the theme in the database and refreshes the message view. For creation, it stores the name and proceeds to the next step in the creation flow. ```python from aiogram.fsm.context import FSMContext from aiogram.types import Message from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardMarkup, InlineKeyboardButton # Assuming 'router', 'ThemeStates', 'get_theme_by_id', 'update_theme', 'get_all_themes', 'create_theme', 'get_theme_by_name' are defined elsewhere @router.message(ThemeStates.name) async def save_theme_name(message: Message, state: FSMContext): data = await state.get_data() theme_id = data.get("theme_id") if theme_id: # EDITING # Delete user input message try: await message.delete() except: # Handle potential errors if message is already deleted or inaccessible pass # Update entity theme = await get_theme_by_id(theme_id) theme.name = message.text await update_theme(theme) # Reload to get fresh data theme = await get_theme_by_id(theme_id) # Build complete edit menu info = f"📚 {theme.name}\n..." builder = InlineKeyboardBuilder() # ... add all edit buttons # Update ORIGINAL message await message.bot.edit_message_text( chat_id=data['edit_chat_id'], message_id=data['edit_message_id'], text=info, reply_markup=builder.as_markup() ) await state.clear() else: # CREATING # Continue creation flow await state.update_data(name=message.text) # ... next step # Pattern 3: Creation completion (as a separate handler or part of the flow) async def complete_creation(message: Message, state: FSMContext): data = await state.get_data() # Delete user input try: await message.delete() except: # Handle potential errors if message is already deleted or inaccessible pass # Create entity theme = await create_theme(name=data['name'], ...) # Load all entities themes = await get_all_themes() # Build list view builder = InlineKeyboardBuilder() for theme in themes: builder.add(...) # Update ORIGINAL message to show list await message.bot.edit_message_text( chat_id=data['create_chat_id'], message_id=data['create_message_id'], text="📚 Управление темами", reply_markup=builder.as_markup() ) await state.clear() # Validation Error Handler (Duplicate Name Check) async def check_duplicate_theme_name(message: Message, state: FSMContext, theme_id=None): data = await state.get_data() new_name = message.text existing = await get_theme_by_name(new_name) if existing and existing.id != theme_id: try: await message.delete() except: # Handle potential errors if message is already deleted or inaccessible pass # Show error in SAME window await message.bot.edit_message_text( chat_id=data['edit_chat_id'], message_id=data['edit_message_id'], text="❌ Ошибка!\n\nТема с таким названием уже существует!\n\nВведите другое название:", reply_markup=InlineKeyboardMarkup(inline_keyboard=[[ InlineKeyboardButton(text="🔙 Отмена", callback_data=f"edit_theme_{theme_id}") ]]) ) # DON'T clear state - user can retry return False # Indicate failure return True # Indicate success ``` -------------------------------- ### Python: Correct Series Attribute Access Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md Highlights the correct way to access attributes of a related 'Series' object from a 'Lesson' object. It contrasts the incorrect method of accessing non-existent direct attributes with the correct method of using the defined relationship attribute (e.g., `lesson.series.year`). ```python # Assuming 'lesson' object is loaded with its 'series' relationship # ❌ WRONG - these fields don't exist anymore on the lesson object directly # year = lesson.series_year # name = lesson.series_name # ✅ CORRECT - use relationship year = lesson.series.year name = lesson.series.name ``` -------------------------------- ### Python: Object Reloading After Updates Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md Illustrates the pattern of reloading an object from the database after it has been updated. This ensures that any changes made and relationships are reflected correctly, preventing stale data issues, especially when accessing related attributes. ```python # Assuming 'update_lesson' and 'get_lesson_by_id' are defined elsewhere await update_lesson(lesson) lesson = await get_lesson_by_id(lesson_id) # Reload with fresh relationships ``` -------------------------------- ### Save Context in FSM State Source: https://github.com/muwahhidun/tg-islamic-bot/blob/main/CLAUDE.md This Python snippet demonstrates how to save user context within a Finite State Machine (FSM) using a state object. It's crucial for preserving navigation information, such as lesson and teacher IDs, along with message identifiers, to enable proper back navigation in a Telegram bot. This pattern is widely used in user-facing handlers. ```python # Pattern: Save context in FSM state await state.update_data( lesson_id=lesson_id, teacher_id=teacher_id, # May be None for general navigation bookmark_message_id=callback.message.message_id, bookmark_chat_id=callback.message.chat.id ) ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.