"""Service classes for private handlers""" # Standard library imports import asyncio import html import random from dataclasses import dataclass from datetime import datetime from pathlib import Path from typing import Any, Callable, Dict, Protocol, Union # Third-party imports from aiogram import types from aiogram.types import FSInputFile from database.models import TelegramPost, User from helper_bot.keyboards import get_reply_keyboard_for_post # Local imports - utilities from helper_bot.utils.helper_func import ( add_in_db_media, check_username_and_full_name, determine_anonymity, get_first_name, get_text_message, prepare_media_group_from_middlewares, send_audio_message, send_media_group_message_to_private_chat, send_photo_message, send_text_message, send_video_message, send_video_note_message, send_voice_message, ) # Local imports - metrics from helper_bot.utils.metrics import ( db_query_time, track_errors, track_file_operations, track_media_processing, track_time, ) from logs.custom_logger import logger class DatabaseProtocol(Protocol): """Protocol for database operations""" async def user_exists(self, user_id: int) -> bool: ... async def add_user(self, user: User) -> None: ... async def update_user_info( self, user_id: int, username: str = None, full_name: str = None ) -> None: ... async def update_user_date(self, user_id: int) -> None: ... async def add_post(self, post: TelegramPost) -> None: ... async def update_stickers_info(self, user_id: int) -> None: ... async def add_message( self, message_text: str, user_id: int, message_id: int, date: int = None ) -> None: ... async def update_helper_message( self, message_id: int, helper_message_id: int ) -> None: ... @dataclass class BotSettings: """Bot configuration settings""" group_for_posts: str group_for_message: str main_public: str group_for_logs: str important_logs: str preview_link: str logs: str test: str class UserService: """Service for user-related operations""" def __init__(self, db: DatabaseProtocol, settings: BotSettings) -> None: self.db = db self.settings = settings @track_time("update_user_activity", "user_service") @track_errors("user_service", "update_user_activity") @db_query_time("update_user_activity", "users", "update") async def update_user_activity(self, user_id: int) -> None: """Update user's last activity timestamp with metrics tracking""" await self.db.update_user_date(user_id) @track_time("ensure_user_exists", "user_service") @track_errors("user_service", "ensure_user_exists") @db_query_time("ensure_user_exists", "users", "insert") async def ensure_user_exists(self, message: types.Message) -> None: """Ensure user exists in database, create if needed with metrics tracking""" user_id = message.from_user.id full_name = message.from_user.full_name # Сохраняем только реальный username, если его нет - сохраняем None/пустую строку username = message.from_user.username first_name = get_first_name(message) is_bot = message.from_user.is_bot language_code = message.from_user.language_code # Create User object with current timestamp current_timestamp = int(datetime.now().timestamp()) user = User( user_id=user_id, first_name=first_name, full_name=full_name, username=username, # Может быть None - это нормально is_bot=is_bot, language_code=language_code, emoji="", has_stickers=False, date_added=current_timestamp, date_changed=current_timestamp, voice_bot_welcome_received=False, ) # Пытаемся создать пользователя (если уже существует - игнорируем) # Это устраняет race condition и упрощает логику await self.db.add_user(user) # Проверяем, нужно ли обновить информацию о существующем пользователе is_need_update = await check_username_and_full_name( user_id, username, full_name, self.db ) if is_need_update: await self.db.update_user_info(user_id, username, full_name) safe_full_name = ( html.escape(full_name) if full_name else "Неизвестный пользователь" ) # Для отображения используем подстановочное значение, но в БД сохраняем только реальный username safe_username = html.escape(username) if username else "Без никнейма" await message.answer( f"Давно не виделись! Вижу что ты изменился;) Теперь буду звать тебя: {safe_full_name} и ник @{safe_username}" ) await message.bot.send_message( chat_id=self.settings.group_for_logs, text=f"Для пользователя: {user_id} обновлены данные в БД.\nНовое имя: {safe_full_name}\nНовый ник:{safe_username}", ) await self.db.update_user_date(user_id) async def log_user_message(self, message: types.Message) -> None: """Forward user message to logs group with metrics tracking""" await message.forward(chat_id=self.settings.group_for_logs) def get_safe_user_info(self, message: types.Message) -> tuple[str, str]: """Get safely escaped user information for logging""" full_name = message.from_user.full_name or "Неизвестный пользователь" username = message.from_user.username or "Без никнейма" return html.escape(full_name), html.escape(username) class PostService: """Service for post-related operations""" def __init__( self, db: DatabaseProtocol, settings: BotSettings, s3_storage=None, scoring_manager=None, ) -> None: self.db = db self.settings = settings self.s3_storage = s3_storage self.scoring_manager = scoring_manager async def _save_media_background( self, sent_message: types.Message, bot_db: Any, s3_storage ) -> None: """Сохраняет медиа в фоне, чтобы не блокировать ответ пользователю""" try: success = await add_in_db_media(sent_message, bot_db, s3_storage) if not success: logger.warning( f"_save_media_background: Не удалось сохранить медиа для поста {sent_message.message_id}" ) except Exception as e: logger.error( f"_save_media_background: Ошибка при сохранении медиа для поста {sent_message.message_id}: {e}" ) async def _get_scores(self, text: str) -> tuple: """ Получает скоры для текста поста. Returns: Tuple (deepseek_score, rag_score, rag_confidence, rag_score_pos_only, ml_scores_json) """ if not self.scoring_manager or not text or not text.strip(): return None, None, None, None, None try: scores = await self.scoring_manager.score_post(text) # Формируем JSON для сохранения в БД import json ml_scores_json = ( json.dumps(scores.to_json_dict()) if scores.has_any_score() else None ) # Получаем данные от RAG rag_confidence = scores.rag.confidence if scores.rag else None rag_score_pos_only = ( scores.rag.metadata.get("rag_score_pos_only") if scores.rag else None ) return ( scores.deepseek_score, scores.rag_score, rag_confidence, rag_score_pos_only, ml_scores_json, ) except Exception as e: logger.error(f"PostService: Ошибка получения скоров: {e}") return None, None, None, None, None async def _save_scores_background( self, message_id: int, ml_scores_json: str ) -> None: """Сохраняет скоры в БД в фоне.""" if ml_scores_json: try: await self.db.update_ml_scores(message_id, ml_scores_json) except Exception as e: logger.error( f"PostService: Ошибка сохранения скоров для {message_id}: {e}" ) async def _get_scores_with_error_handling(self, text: str) -> tuple: """ Получает скоры для текста поста с обработкой ошибок. Returns: Tuple (deepseek_score, rag_score, rag_confidence, rag_score_pos_only, ml_scores_json, error_message) error_message будет None если все ок, или строка с описанием ошибки """ if not self.scoring_manager: # Скоры выключены в .env - это нормально return None, None, None, None, None, None if not text or not text.strip(): return None, None, None, None, None, None try: scores = await self.scoring_manager.score_post(text) # Формируем JSON для сохранения в БД import json ml_scores_json = ( json.dumps(scores.to_json_dict()) if scores.has_any_score() else None ) # Получаем данные от RAG rag_confidence = scores.rag.confidence if scores.rag else None rag_score_pos_only = ( scores.rag.metadata.get("rag_score_pos_only") if scores.rag else None ) return ( scores.deepseek_score, scores.rag_score, rag_confidence, rag_score_pos_only, ml_scores_json, None, ) except Exception as e: logger.error(f"PostService: Ошибка получения скоров: {e}") # Возвращаем частичные скоры если есть, или сообщение об ошибке error_message = "Не удалось рассчитать скоры" return None, None, None, None, None, error_message @track_time("_process_post_background", "post_service") @track_errors("post_service", "_process_post_background") async def _process_post_background( self, message: types.Message, first_name: str, content_type: str, album: Union[list, None] = None, ) -> None: """ Обрабатывает пост в фоне: получает скоры, отправляет в группу модерации, сохраняет в БД. Args: message: Сообщение от пользователя first_name: Имя пользователя content_type: Тип контента ('text', 'photo', 'video', 'audio', 'voice', 'video_note', 'media_group') album: Список сообщений медиагруппы (только для media_group) """ try: # Определяем исходный текст для скоринга и определения анонимности original_raw_text = "" if content_type == "text": original_raw_text = message.text or "" elif content_type == "media_group": original_raw_text = ( album[0].caption or "" if album and album[0].caption else "" ) else: original_raw_text = message.caption or "" # Получаем скоры с обработкой ошибок ( deepseek_score, rag_score, rag_confidence, rag_score_pos_only, ml_scores_json, error_message, ) = await self._get_scores_with_error_handling(original_raw_text) # Формируем текст для поста (с сообщением об ошибке если есть) text_for_post = original_raw_text if error_message: # Для текстовых постов добавляем в конец текста if content_type == "text": text_for_post = f"{original_raw_text}\n\n⚠️ {error_message}" # Для медиа добавляем в caption elif content_type in ("photo", "video", "audio") and original_raw_text: text_for_post = f"{original_raw_text}\n\n⚠️ {error_message}" # Формируем текст/caption с учетом скоров post_text = "" if text_for_post or content_type == "text": logger.debug( f"PostService._process_post_background: Передача скоров в get_text_message - " f"rag_score={rag_score} (type: {type(rag_score).__name__ if rag_score is not None else 'None'}), " f"rag_score_pos_only={rag_score_pos_only} (type: {type(rag_score_pos_only).__name__ if rag_score_pos_only is not None else 'None'}), " f"rag_confidence={rag_confidence} (type: {type(rag_confidence).__name__ if rag_confidence is not None else 'None'}), " f"content_type={content_type}, message_id={message.message_id}" ) post_text = get_text_message( text_for_post.lower() if text_for_post else "", first_name, message.from_user.username, deepseek_score=deepseek_score, rag_score=rag_score, rag_confidence=rag_confidence, rag_score_pos_only=rag_score_pos_only, ) # Определяем анонимность по исходному тексту (без сообщения об ошибке) is_anonymous = determine_anonymity(original_raw_text) markup = get_reply_keyboard_for_post() sent_message = None # Отправляем пост в группу модерации в зависимости от типа if content_type == "text": sent_message = await send_text_message( self.settings.group_for_posts, message, post_text, markup ) elif content_type == "photo": sent_message = await send_photo_message( self.settings.group_for_posts, message, message.photo[-1].file_id, post_text, markup, ) elif content_type == "video": sent_message = await send_video_message( self.settings.group_for_posts, message, message.video.file_id, post_text, markup, ) elif content_type == "audio": sent_message = await send_audio_message( self.settings.group_for_posts, message, message.audio.file_id, post_text, markup, ) elif content_type == "voice": sent_message = await send_voice_message( self.settings.group_for_posts, message, message.voice.file_id, markup, ) elif content_type == "video_note": sent_message = await send_video_note_message( self.settings.group_for_posts, message, message.video_note.file_id, markup, ) elif content_type == "media_group": # Для медиагруппы используем специальную обработку # Передаем ml_scores_json для сохранения в БД await self._process_media_group_background( message, album, first_name, post_text, is_anonymous, original_raw_text, ml_scores_json, ) return else: logger.error( f"PostService: Неподдерживаемый тип контента: {content_type}" ) return if not sent_message: logger.error( f"PostService: Не удалось отправить пост типа {content_type}" ) return # Сохраняем пост в БД (сохраняем исходный текст, без сообщения об ошибке) post = TelegramPost( message_id=sent_message.message_id, text=original_raw_text, author_id=message.from_user.id, created_at=int(datetime.now().timestamp()), is_anonymous=is_anonymous, ) await self.db.add_post(post) # Сохраняем медиа и скоры в фоне if content_type in ("photo", "video", "audio", "voice", "video_note"): asyncio.create_task( self._save_media_background(sent_message, self.db, self.s3_storage) ) if ml_scores_json: asyncio.create_task( self._save_scores_background( sent_message.message_id, ml_scores_json ) ) except Exception as e: logger.error( f"PostService: Критическая ошибка в _process_post_background для {content_type}: {e}" ) async def _process_media_group_background( self, message: types.Message, album: list, first_name: str, post_caption: str, is_anonymous: bool, original_raw_text: str, ml_scores_json: str = None, ) -> None: """Обрабатывает медиагруппу в фоне""" try: media_group = await prepare_media_group_from_middlewares( album, post_caption ) media_group_message_ids = await send_media_group_message_to_private_chat( self.settings.group_for_posts, message, media_group, self.db, None, self.s3_storage, ) main_post_id = media_group_message_ids[-1] main_post = TelegramPost( message_id=main_post_id, text=original_raw_text, author_id=message.from_user.id, created_at=int(datetime.now().timestamp()), is_anonymous=is_anonymous, ) await self.db.add_post(main_post) # Сохраняем скоры в фоне (если они были получены) if ml_scores_json: asyncio.create_task( self._save_scores_background(main_post_id, ml_scores_json) ) for msg_id in media_group_message_ids: await self.db.add_message_link(main_post_id, msg_id) await asyncio.sleep(0.2) markup = get_reply_keyboard_for_post() helper_message = await send_text_message( self.settings.group_for_posts, message, "^", markup ) helper_message_id = helper_message.message_id helper_post = TelegramPost( message_id=helper_message_id, text="^", author_id=message.from_user.id, helper_text_message_id=main_post_id, created_at=int(datetime.now().timestamp()), ) await self.db.add_post(helper_post) await self.db.update_helper_message( message_id=main_post_id, helper_message_id=helper_message_id ) except Exception as e: logger.error(f"PostService: Ошибка в _process_media_group_background: {e}") @track_time("handle_text_post", "post_service") @track_errors("post_service", "handle_text_post") @db_query_time("handle_text_post", "posts", "insert") async def handle_text_post(self, message: types.Message, first_name: str) -> None: """Handle text post submission""" raw_text = message.text or "" # Получаем скоры для текста ( deepseek_score, rag_score, rag_confidence, rag_score_pos_only, ml_scores_json, ) = await self._get_scores(raw_text) logger.debug( f"PostService.handle_text_post: Передача скоров в get_text_message - " f"rag_score={rag_score} (type: {type(rag_score).__name__ if rag_score is not None else 'None'}), " f"rag_score_pos_only={rag_score_pos_only} (type: {type(rag_score_pos_only).__name__ if rag_score_pos_only is not None else 'None'}), " f"rag_confidence={rag_confidence} (type: {type(rag_confidence).__name__ if rag_confidence is not None else 'None'}), " f"message_id={message.message_id}" ) # Формируем текст с учетом скоров post_text = get_text_message( message.text.lower(), first_name, message.from_user.username, deepseek_score=deepseek_score, rag_score=rag_score, rag_confidence=rag_confidence, rag_score_pos_only=rag_score_pos_only, ) markup = get_reply_keyboard_for_post() sent_message = await send_text_message( self.settings.group_for_posts, message, post_text, markup ) # Определяем анонимность is_anonymous = determine_anonymity(raw_text) post = TelegramPost( message_id=sent_message.message_id, text=raw_text, author_id=message.from_user.id, created_at=int(datetime.now().timestamp()), is_anonymous=is_anonymous, ) await self.db.add_post(post) # Сохраняем скоры в фоне if ml_scores_json: asyncio.create_task( self._save_scores_background(sent_message.message_id, ml_scores_json) ) @track_time("handle_photo_post", "post_service") @track_errors("post_service", "handle_photo_post") @db_query_time("handle_photo_post", "posts", "insert") async def handle_photo_post(self, message: types.Message, first_name: str) -> None: """Handle photo post submission""" raw_caption = message.caption or "" # Получаем скоры для текста ( deepseek_score, rag_score, rag_confidence, rag_score_pos_only, ml_scores_json, ) = await self._get_scores(raw_caption) logger.debug( f"PostService.handle_photo_post: Передача скоров в get_text_message - " f"rag_score={rag_score} (type: {type(rag_score).__name__ if rag_score is not None else 'None'}), " f"rag_score_pos_only={rag_score_pos_only} (type: {type(rag_score_pos_only).__name__ if rag_score_pos_only is not None else 'None'}), " f"rag_confidence={rag_confidence} (type: {type(rag_confidence).__name__ if rag_confidence is not None else 'None'}), " f"message_id={message.message_id}" ) post_caption = "" if message.caption: post_caption = get_text_message( message.caption.lower(), first_name, message.from_user.username, deepseek_score=deepseek_score, rag_score=rag_score, rag_confidence=rag_confidence, rag_score_pos_only=rag_score_pos_only, ) markup = get_reply_keyboard_for_post() sent_message = await send_photo_message( self.settings.group_for_posts, message, message.photo[-1].file_id, post_caption, markup, ) # Определяем анонимность is_anonymous = determine_anonymity(raw_caption) post = TelegramPost( message_id=sent_message.message_id, text=raw_caption, author_id=message.from_user.id, created_at=int(datetime.now().timestamp()), is_anonymous=is_anonymous, ) await self.db.add_post(post) # Сохраняем медиа и скоры в фоне asyncio.create_task( self._save_media_background(sent_message, self.db, self.s3_storage) ) if ml_scores_json: asyncio.create_task( self._save_scores_background(sent_message.message_id, ml_scores_json) ) @track_time("handle_video_post", "post_service") @track_errors("post_service", "handle_video_post") @db_query_time("handle_video_post", "posts", "insert") async def handle_video_post(self, message: types.Message, first_name: str) -> None: """Handle video post submission""" raw_caption = message.caption or "" # Получаем скоры для текста ( deepseek_score, rag_score, rag_confidence, rag_score_pos_only, ml_scores_json, ) = await self._get_scores(raw_caption) logger.debug( f"PostService.handle_video_post: Передача скоров в get_text_message - " f"rag_score={rag_score} (type: {type(rag_score).__name__ if rag_score is not None else 'None'}), " f"rag_score_pos_only={rag_score_pos_only} (type: {type(rag_score_pos_only).__name__ if rag_score_pos_only is not None else 'None'}), " f"rag_confidence={rag_confidence} (type: {type(rag_confidence).__name__ if rag_confidence is not None else 'None'}), " f"message_id={message.message_id}" ) post_caption = "" if message.caption: post_caption = get_text_message( message.caption.lower(), first_name, message.from_user.username, deepseek_score=deepseek_score, rag_score=rag_score, rag_confidence=rag_confidence, rag_score_pos_only=rag_score_pos_only, ) markup = get_reply_keyboard_for_post() sent_message = await send_video_message( self.settings.group_for_posts, message, message.video.file_id, post_caption, markup, ) # Определяем анонимность is_anonymous = determine_anonymity(raw_caption) post = TelegramPost( message_id=sent_message.message_id, text=raw_caption, author_id=message.from_user.id, created_at=int(datetime.now().timestamp()), is_anonymous=is_anonymous, ) await self.db.add_post(post) # Сохраняем медиа и скоры в фоне asyncio.create_task( self._save_media_background(sent_message, self.db, self.s3_storage) ) if ml_scores_json: asyncio.create_task( self._save_scores_background(sent_message.message_id, ml_scores_json) ) @track_time("handle_video_note_post", "post_service") @track_errors("post_service", "handle_video_note_post") @db_query_time("handle_video_note_post", "posts", "insert") async def handle_video_note_post(self, message: types.Message) -> None: """Handle video note post submission""" markup = get_reply_keyboard_for_post() sent_message = await send_video_note_message( self.settings.group_for_posts, message, message.video_note.file_id, markup ) # Сохраняем пустую строку, так как video_note не имеет caption raw_caption = "" is_anonymous = determine_anonymity(raw_caption) post = TelegramPost( message_id=sent_message.message_id, text=raw_caption, author_id=message.from_user.id, created_at=int(datetime.now().timestamp()), is_anonymous=is_anonymous, ) await self.db.add_post(post) # Сохраняем медиа в фоне, чтобы не блокировать ответ пользователю asyncio.create_task( self._save_media_background(sent_message, self.db, self.s3_storage) ) @track_time("handle_audio_post", "post_service") @track_errors("post_service", "handle_audio_post") @db_query_time("handle_audio_post", "posts", "insert") async def handle_audio_post(self, message: types.Message, first_name: str) -> None: """Handle audio post submission""" raw_caption = message.caption or "" # Получаем скоры для текста ( deepseek_score, rag_score, rag_confidence, rag_score_pos_only, ml_scores_json, ) = await self._get_scores(raw_caption) logger.debug( f"PostService.handle_audio_post: Передача скоров в get_text_message - " f"rag_score={rag_score} (type: {type(rag_score).__name__ if rag_score is not None else 'None'}), " f"rag_score_pos_only={rag_score_pos_only} (type: {type(rag_score_pos_only).__name__ if rag_score_pos_only is not None else 'None'}), " f"rag_confidence={rag_confidence} (type: {type(rag_confidence).__name__ if rag_confidence is not None else 'None'}), " f"message_id={message.message_id}" ) post_caption = "" if message.caption: post_caption = get_text_message( message.caption.lower(), first_name, message.from_user.username, deepseek_score=deepseek_score, rag_score=rag_score, rag_confidence=rag_confidence, rag_score_pos_only=rag_score_pos_only, ) markup = get_reply_keyboard_for_post() sent_message = await send_audio_message( self.settings.group_for_posts, message, message.audio.file_id, post_caption, markup, ) # Определяем анонимность is_anonymous = determine_anonymity(raw_caption) post = TelegramPost( message_id=sent_message.message_id, text=raw_caption, author_id=message.from_user.id, created_at=int(datetime.now().timestamp()), is_anonymous=is_anonymous, ) await self.db.add_post(post) # Сохраняем медиа и скоры в фоне asyncio.create_task( self._save_media_background(sent_message, self.db, self.s3_storage) ) if ml_scores_json: asyncio.create_task( self._save_scores_background(sent_message.message_id, ml_scores_json) ) @track_time("handle_voice_post", "post_service") @track_errors("post_service", "handle_voice_post") @db_query_time("handle_voice_post", "posts", "insert") async def handle_voice_post(self, message: types.Message) -> None: """Handle voice post submission""" markup = get_reply_keyboard_for_post() sent_message = await send_voice_message( self.settings.group_for_posts, message, message.voice.file_id, markup ) # Сохраняем пустую строку, так как voice не имеет caption raw_caption = "" is_anonymous = determine_anonymity(raw_caption) post = TelegramPost( message_id=sent_message.message_id, text=raw_caption, author_id=message.from_user.id, created_at=int(datetime.now().timestamp()), is_anonymous=is_anonymous, ) await self.db.add_post(post) # Сохраняем медиа в фоне, чтобы не блокировать ответ пользователю asyncio.create_task( self._save_media_background(sent_message, self.db, self.s3_storage) ) @track_time("handle_media_group_post", "post_service") @track_errors("post_service", "handle_media_group_post") @db_query_time("handle_media_group_post", "posts", "insert") @track_media_processing("media_group") async def handle_media_group_post( self, message: types.Message, album: list, first_name: str ) -> None: """Handle media group post submission""" post_caption = " " raw_caption = "" ml_scores_json = None if album and album[0].caption: raw_caption = album[0].caption or "" # Получаем скоры для текста ( deepseek_score, rag_score, rag_confidence, rag_score_pos_only, ml_scores_json, ) = await self._get_scores(raw_caption) logger.debug( f"PostService.handle_media_group_post: Передача скоров в get_text_message - " f"rag_score={rag_score} (type: {type(rag_score).__name__ if rag_score is not None else 'None'}), " f"rag_score_pos_only={rag_score_pos_only} (type: {type(rag_score_pos_only).__name__ if rag_score_pos_only is not None else 'None'}), " f"rag_confidence={rag_confidence} (type: {type(rag_confidence).__name__ if rag_confidence is not None else 'None'}), " f"message_id={message.message_id}" ) post_caption = get_text_message( album[0].caption.lower(), first_name, message.from_user.username, deepseek_score=deepseek_score, rag_score=rag_score, rag_confidence=rag_confidence, rag_score_pos_only=rag_score_pos_only, ) is_anonymous = determine_anonymity(raw_caption) media_group = await prepare_media_group_from_middlewares(album, post_caption) media_group_message_ids = await send_media_group_message_to_private_chat( self.settings.group_for_posts, message, media_group, self.db, None, self.s3_storage, ) main_post_id = media_group_message_ids[-1] main_post = TelegramPost( message_id=main_post_id, text=raw_caption, author_id=message.from_user.id, created_at=int(datetime.now().timestamp()), is_anonymous=is_anonymous, ) await self.db.add_post(main_post) # Сохраняем скоры в фоне if ml_scores_json: asyncio.create_task( self._save_scores_background(main_post_id, ml_scores_json) ) for msg_id in media_group_message_ids: await self.db.add_message_link(main_post_id, msg_id) await asyncio.sleep(0.2) markup = get_reply_keyboard_for_post() helper_message = await send_text_message( self.settings.group_for_posts, message, "^", markup ) helper_message_id = helper_message.message_id helper_post = TelegramPost( message_id=helper_message_id, text="^", author_id=message.from_user.id, helper_text_message_id=main_post_id, created_at=int(datetime.now().timestamp()), ) await self.db.add_post(helper_post) await self.db.update_helper_message( message_id=main_post_id, helper_message_id=helper_message_id ) @track_time("process_post", "post_service") @track_errors("post_service", "process_post") @track_media_processing("media_group") async def process_post( self, message: types.Message, album: Union[list, None] = None ) -> None: """ Запускает обработку поста в фоне. Не блокирует выполнение - сразу возвращает управление. """ first_name = get_first_name(message) # Определяем тип контента content_type = ( "media_group" if message.media_group_id is not None else message.content_type ) # Запускаем фоновую обработку asyncio.create_task( self._process_post_background(message, first_name, content_type, album) ) class StickerService: """Service for sticker-related operations""" def __init__(self, settings: BotSettings) -> None: self.settings = settings @track_time("send_random_hello_sticker", "sticker_service") @track_errors("sticker_service", "send_random_hello_sticker") @track_file_operations("sticker") async def send_random_hello_sticker(self, message: types.Message) -> None: """Send random hello sticker with metrics tracking""" name_stick_hello = list(Path("Stick").rglob("Hello_*")) if not name_stick_hello: return random_stick_hello = random.choice(name_stick_hello) random_stick_hello = FSInputFile(path=random_stick_hello) await message.answer_sticker(random_stick_hello) await asyncio.sleep(0.3) @track_time("send_random_goodbye_sticker", "sticker_service") @track_errors("sticker_service", "send_random_goodbye_sticker") @track_file_operations("sticker") async def send_random_goodbye_sticker(self, message: types.Message) -> None: """Send random goodbye sticker with metrics tracking""" name_stick_bye = list(Path("Stick").rglob("Universal_*")) if not name_stick_bye: return random_stick_bye = random.choice(name_stick_bye) random_stick_bye = FSInputFile(path=random_stick_bye) await message.answer_sticker(random_stick_bye)