import asyncio from datetime import datetime from pathlib import Path from aiogram import F, Router, types from aiogram.filters import Command, MagicData, StateFilter from aiogram.fsm.context import FSMContext from aiogram.types import FSInputFile from helper_bot.filters.main import ChatTypeFilter from helper_bot.handlers.private.constants import BUTTON_TEXTS, FSM_STATES from helper_bot.handlers.voice.constants import * from helper_bot.handlers.voice.services import VoiceBotService from helper_bot.handlers.voice.utils import (get_last_message_text, get_user_emoji_safe, validate_voice_message) from helper_bot.keyboards import get_reply_keyboard from helper_bot.keyboards.keyboards import (get_main_keyboard, get_reply_keyboard_for_voice) from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware from helper_bot.middlewares.dependencies_middleware import \ DependenciesMiddleware from helper_bot.utils import messages from helper_bot.utils.helper_func import (check_user_emoji, get_first_name, send_voice_message, update_user_info) # Local imports - metrics from helper_bot.utils.metrics import (db_query_time, track_errors, track_file_operations, track_time) from logs.custom_logger import logger class VoiceHandlers: def __init__(self, db, settings): self.db = db.get_db() if hasattr(db, 'get_db') else db self.settings = settings self.router = Router() self._setup_handlers() self._setup_middleware() def _setup_middleware(self): self.router.message.middleware(DependenciesMiddleware()) self.router.message.middleware(BlacklistMiddleware()) def _setup_handlers(self): self.router.message.register( self.cancel_handler, ChatTypeFilter(chat_type=["private"]), F.text == "Отменить" ) # Обработчик кнопки "Голосовой бот" self.router.message.register( self.voice_bot_button_handler, ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["VOICE_BOT"] ) # Команды self.router.message.register( self.restart_function, ChatTypeFilter(chat_type=["private"]), Command(CMD_RESTART) ) self.router.message.register( self.handle_emoji_message, ChatTypeFilter(chat_type=["private"]), Command(CMD_EMOJI) ) self.router.message.register( self.help_function, ChatTypeFilter(chat_type=["private"]), Command(CMD_HELP) ) self.router.message.register( self.start, ChatTypeFilter(chat_type=["private"]), Command(CMD_START) ) # Дополнительные команды self.router.message.register( self.refresh_listen_function, ChatTypeFilter(chat_type=["private"]), Command(CMD_REFRESH) ) # Обработчики состояний и кнопок self.router.message.register( self.standup_write, StateFilter(STATE_START), ChatTypeFilter(chat_type=["private"]), F.text == BTN_SPEAK ) self.router.message.register( self.suggest_voice, StateFilter(STATE_STANDUP_WRITE), ChatTypeFilter(chat_type=["private"]), ) self.router.message.register( self.standup_listen_audio, StateFilter(STATE_START), ChatTypeFilter(chat_type=["private"]), F.text == BTN_LISTEN ) # Новые обработчики кнопок self.router.message.register( self.refresh_listen_function, ChatTypeFilter(chat_type=["private"]), F.text == "🔄Сбросить прослушивания" ) self.router.message.register( self.handle_emoji_message, ChatTypeFilter(chat_type=["private"]), F.text == "😊Узнать эмодзи" ) @track_time("voice_bot_button_handler", "voice_handlers") @track_errors("voice_handlers", "voice_bot_button_handler") async def voice_bot_button_handler(self, message: types.Message, state: FSMContext, bot_db: MagicData("bot_db"), settings: MagicData("settings")): """Обработчик кнопки 'Голосовой бот' из основной клавиатуры""" logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}) нажал кнопку 'Голосовой бот'") try: # Проверяем, получал ли пользователь приветственное сообщение welcome_received = await bot_db.check_voice_bot_welcome_received(message.from_user.id) logger.info(f"Пользователь {message.from_user.id}: welcome_received = {welcome_received}") if welcome_received: # Если уже получал приветствие, вызываем restart_function logger.info(f"Пользователь {message.from_user.id}: вызываем restart_function") await self.restart_function(message, state, bot_db, settings) else: # Если не получал, вызываем start logger.info(f"Пользователь {message.from_user.id}: вызываем start") await self.start(message, state, bot_db, settings) except Exception as e: logger.error(f"Ошибка при проверке приветственного сообщения для пользователя {message.from_user.id}: {e}") # В случае ошибки вызываем start await self.start(message, state, bot_db, settings) @track_time("restart_function", "voice_handlers") @track_errors("voice_handlers", "restart_function") async def restart_function( self, message: types.Message, state: FSMContext, bot_db: MagicData("bot_db"), settings: MagicData("settings") ): logger.info(f"Пользователь {message.from_user.id}: вызывается функция restart_function") await message.forward(chat_id=settings['Telegram']['group_for_logs']) await update_user_info(VOICE_BOT_NAME, message) await check_user_emoji(message) markup = get_main_keyboard() await message.answer(text='🎤 Записывайся или слушай!', reply_markup=markup) await state.set_state(STATE_START) @track_time("handle_emoji_message", "voice_handlers") @track_errors("voice_handlers", "handle_emoji_message") async def handle_emoji_message( self, message: types.Message, state: FSMContext, settings: MagicData("settings") ): logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}) запросил информацию об эмодзи") await message.forward(chat_id=settings['Telegram']['group_for_logs']) user_emoji = await check_user_emoji(message) await state.set_state(STATE_START) if user_emoji is not None: await message.answer(f'Твоя эмодзя - {user_emoji}', parse_mode='HTML') @track_time("help_function", "voice_handlers") @track_errors("voice_handlers", "help_function") async def help_function( self, message: types.Message, state: FSMContext, settings: MagicData("settings") ): logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}) вызвал функцию help_function") await message.forward(chat_id=settings['Telegram']['group_for_logs']) await update_user_info(VOICE_BOT_NAME, message) help_message = messages.get_message(get_first_name(message), 'HELP_MESSAGE') await message.answer( text=help_message, disable_web_page_preview=not settings['Telegram']['preview_link'] ) await state.set_state(STATE_START) @track_time("start", "voice_handlers") @track_errors("voice_handlers", "start") @db_query_time("mark_voice_bot_welcome_received", "audio_moderate", "update") async def start( self, message: types.Message, state: FSMContext, bot_db: MagicData("bot_db"), settings: MagicData("settings") ): logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}): вызывается функция start") await state.set_state(STATE_START) await message.forward(chat_id=settings['Telegram']['group_for_logs']) await update_user_info(VOICE_BOT_NAME, message) user_emoji = await get_user_emoji_safe(bot_db, message.from_user.id) # Создаем сервис и отправляем приветственные сообщения voice_service = VoiceBotService(bot_db, settings) await voice_service.send_welcome_messages(message, user_emoji) logger.info(f"Приветственные сообщения отправлены пользователю {message.from_user.id}") # Отмечаем, что пользователь получил приветственное сообщение try: await bot_db.mark_voice_bot_welcome_received(message.from_user.id) logger.info(f"Пользователь {message.from_user.id}: отмечен как получивший приветствие") except Exception as e: logger.error(f"Ошибка при отметке получения приветствия для пользователя {message.from_user.id}: {e}") @track_time("cancel_handler", "voice_handlers") @track_errors("voice_handlers", "cancel_handler") async def cancel_handler( self, message: types.Message, state: FSMContext, bot_db: MagicData("bot_db"), settings: MagicData("settings") ): """Обработчик кнопки 'Отменить' - возвращает в начальное состояние""" await message.forward(chat_id=settings['Telegram']['group_for_logs']) await update_user_info(VOICE_BOT_NAME, message) markup = await get_reply_keyboard(self.db, message.from_user.id) await message.answer(text='Добро пожаловать в меню!', reply_markup=markup, parse_mode='HTML') await state.set_state(FSM_STATES["START"]) logger.info(f"Пользователь {message.from_user.id} возвращен в главное меню") @track_time("refresh_listen_function", "voice_handlers") @track_errors("voice_handlers", "refresh_listen_function") async def refresh_listen_function( self, message: types.Message, state: FSMContext, bot_db: MagicData("bot_db"), settings: MagicData("settings") ): logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}) вызвал функцию refresh_listen_function") await message.forward(chat_id=settings['Telegram']['group_for_logs']) await update_user_info(VOICE_BOT_NAME, message) markup = get_main_keyboard() # Очищаем прослушивания через сервис voice_service = VoiceBotService(bot_db, settings) await voice_service.clear_user_listenings(message.from_user.id) listenings_cleared_message = messages.get_message(get_first_name(message), 'LISTENINGS_CLEARED_MESSAGE') await message.answer( text=listenings_cleared_message, disable_web_page_preview=not settings['Telegram']['preview_link'], reply_markup=markup ) await state.set_state(STATE_START) @track_time("standup_write", "voice_handlers") @track_errors("voice_handlers", "standup_write") async def standup_write( self, message: types.Message, state: FSMContext, bot_db: MagicData("bot_db"), settings: MagicData("settings") ): logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}) вызвал функцию standup_write") await message.forward(chat_id=settings['Telegram']['group_for_logs']) markup = types.ReplyKeyboardRemove() record_voice_message = messages.get_message(get_first_name(message), 'RECORD_VOICE_MESSAGE') await message.answer(text=record_voice_message, reply_markup=markup) try: message_with_date = await get_last_message_text(bot_db) if message_with_date: await message.answer(text=message_with_date, parse_mode="html") except Exception as e: logger.error(f'Не удалось получить дату последнего сообщения для пользователя {message.from_user.id}: {e}') await state.set_state(STATE_STANDUP_WRITE) @track_time("suggest_voice", "voice_handlers") @track_errors("voice_handlers", "suggest_voice") async def suggest_voice( self, message: types.Message, state: FSMContext, bot_db: MagicData("bot_db"), settings: MagicData("settings") ): logger.info( f"Вызов функции suggest_voice. Пользователь: {message.from_user.id} Имя автора сообщения: {message.from_user.full_name}" ) await message.forward(chat_id=settings['Telegram']['group_for_logs']) markup = get_main_keyboard() if await validate_voice_message(message): markup_for_voice = get_reply_keyboard_for_voice() # Отправляем аудио в приватный канал sent_message = await send_voice_message( settings['Telegram']['group_for_posts'], message, message.voice.file_id, markup_for_voice ) logger.info(f"Голосовое сообщение пользователя {message.from_user.id} отправлено в группу постов (message_id: {sent_message.message_id})") # Сохраняем в базу инфо о посте await bot_db.set_user_id_and_message_id_for_voice_bot(sent_message.message_id, message.from_user.id) # Отправляем юзеру ответ и возвращаем его в меню voice_saved_message = messages.get_message(get_first_name(message), 'VOICE_SAVED_MESSAGE') await message.answer(text=voice_saved_message, reply_markup=markup) await state.set_state(STATE_START) else: logger.warning(f"Голосовое сообщение пользователя {message.from_user.id} не прошло валидацию") unknown_content_message = messages.get_message(get_first_name(message), 'UNKNOWN_CONTENT_MESSAGE') await message.forward(chat_id=settings['Telegram']['group_for_logs']) await message.answer(text=unknown_content_message, reply_markup=markup) await state.set_state(STATE_STANDUP_WRITE) @track_time("standup_listen_audio", "voice_handlers") @track_errors("voice_handlers", "standup_listen_audio") @track_file_operations("voice") @db_query_time("standup_listen_audio", "audio_moderate", "mixed") async def standup_listen_audio( self, message: types.Message, bot_db: MagicData("bot_db"), settings: MagicData("settings") ): logger.info(f"Пользователь {message.from_user.id} ({message.from_user.full_name}) запросил прослушивание аудио") markup = get_main_keyboard() # Создаем сервис для работы с аудио voice_service = VoiceBotService(bot_db, settings) try: #TODO: удалить логику из хендлера # Получаем случайное аудио audio_data = await voice_service.get_random_audio(message.from_user.id) if not audio_data: logger.warning(f"Для пользователя {message.from_user.id} не найдено доступных аудио для прослушивания") no_audio_message = messages.get_message(get_first_name(message), 'NO_AUDIO_MESSAGE') await message.answer(text=no_audio_message, reply_markup=markup) try: message_with_date = await get_last_message_text(bot_db) if message_with_date: await message.answer(text=message_with_date, parse_mode="html") except Exception as e: logger.error(f'Не удалось получить последнюю дату для пользователя {message.from_user.id}: {e}') return audio_for_user, date_added, user_emoji = audio_data # Получаем путь к файлу path = Path(f'{VOICE_USERS_DIR}/{audio_for_user}.ogg') # Проверяем существование файла if not path.exists(): logger.error(f"Файл не найден: {path} для пользователя {message.from_user.id}") # Дополнительная диагностика logger.error(f"Директория {VOICE_USERS_DIR} существует: {Path(VOICE_USERS_DIR).exists()}") if Path(VOICE_USERS_DIR).exists(): files_in_dir = list(Path(VOICE_USERS_DIR).glob("*.ogg")) logger.error(f"Файлы в директории: {[f.name for f in files_in_dir]}") await message.answer( text="Файл аудио не найден. Обратитесь к администратору.", reply_markup=markup ) return # Проверяем размер файла if path.stat().st_size == 0: logger.error(f"Файл пустой: {path} для пользователя {message.from_user.id}") await message.answer( text="Файл аудио поврежден. Обратитесь к администратору.", reply_markup=markup ) return voice = FSInputFile(path) # Формируем подпись if user_emoji: caption = f'{user_emoji}\nДата записи: {date_added}' else: caption = f'Дата записи: {date_added}' logger.info(f"Подготовлено голосовое сообщение для пользователя {message.from_user.id}: {caption}") try: from helper_bot.utils.rate_limiter import send_with_rate_limit async def _send_voice(): return await message.bot.send_voice( chat_id=message.chat.id, voice=voice, caption=caption, reply_markup=markup ) await send_with_rate_limit(_send_voice, message.chat.id) # Маркируем сообщение как прослушанное только после успешной отправки await voice_service.mark_audio_as_listened(audio_for_user, message.from_user.id) # Получаем количество оставшихся аудио только после успешной отправки remaining_count = await voice_service.get_remaining_audio_count(message.from_user.id) await message.answer( text=f'Осталось непрослушанных: {remaining_count}', reply_markup=markup ) except Exception as voice_error: if "VOICE_MESSAGES_FORBIDDEN" in str(voice_error): # Если голосовые сообщения запрещены, отправляем информативное сообщение logger.warning(f"Пользователь {message.from_user.id} запретил получение голосовых сообщений") privacy_message = "🔇 К сожалению, у тебя закрыт доступ к получению голосовых сообщений.\n\nДля продолжения взаимодействия с ботом необходимо дать возможность мне присылать войсы в настройках приватности Telegram.\n\n💡 Как это сделать:\n1. Открой настройки Telegram\n2. Перейди в 'Конфиденциальность и безопасность'\n3. Выбери 'Голосовые сообщения'\n4. Разреши получение от 'Всех' или добавь меня в исключения" await message.answer(text=privacy_message, reply_markup=markup) return # Выходим без записи о прослушивании else: logger.error(f"Ошибка при отправке голосового сообщения пользователю {message.from_user.id}: {voice_error}") raise voice_error except Exception as e: logger.error(f"Ошибка при прослушивании аудио для пользователя {message.from_user.id}: {e}") await message.answer( text="Произошла ошибка при получении аудио. Попробуйте позже.", reply_markup=markup )