Files
telegram-helper-bot/helper_bot/handlers/voice/voice_handler.py
2026-02-02 00:54:23 +03:00

530 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"Осталось непрослушанных: <b>{remaining_count}</b>",
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,
)