489 lines
25 KiB
Python
489 lines
25 KiB
Python
import asyncio
|
||
import os
|
||
import random
|
||
import traceback
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import List, Optional, Tuple
|
||
|
||
from aiogram.types import FSInputFile
|
||
from helper_bot.handlers.voice.constants import (MESSAGE_DELAY_1,
|
||
MESSAGE_DELAY_2,
|
||
MESSAGE_DELAY_3,
|
||
MESSAGE_DELAY_4, STICK_DIR,
|
||
STICK_PATTERN, STICKER_DELAY,
|
||
VOICE_USERS_DIR)
|
||
from helper_bot.handlers.voice.exceptions import (AudioProcessingError,
|
||
DatabaseError,
|
||
FileOperationError,
|
||
VoiceMessageError)
|
||
# Local imports - metrics
|
||
from helper_bot.utils.metrics import db_query_time, track_errors, track_time
|
||
from logs.custom_logger import logger
|
||
|
||
|
||
class VoiceMessage:
|
||
"""Модель голосового сообщения"""
|
||
|
||
def __init__(
|
||
self, file_name: str, user_id: int, date_added: datetime, file_id: int
|
||
):
|
||
self.file_name = file_name
|
||
self.user_id = user_id
|
||
self.date_added = date_added
|
||
self.file_id = file_id
|
||
|
||
|
||
class VoiceBotService:
|
||
"""Сервис для работы с голосовыми сообщениями"""
|
||
|
||
def __init__(self, bot_db, settings):
|
||
self.bot_db = bot_db
|
||
self.settings = settings
|
||
|
||
@track_time("get_welcome_sticker", "voice_bot_service")
|
||
@track_errors("voice_bot_service", "get_welcome_sticker")
|
||
async def get_welcome_sticker(self) -> Optional[FSInputFile]:
|
||
"""Получить случайный приветственный стикер"""
|
||
try:
|
||
name_stick_hello = list(Path(STICK_DIR).rglob(STICK_PATTERN))
|
||
if not name_stick_hello:
|
||
return None
|
||
|
||
random_stick_hello = random.choice(name_stick_hello)
|
||
random_stick_hello = FSInputFile(path=random_stick_hello)
|
||
logger.info(
|
||
f"Стикер успешно получен. Наименование стикера: {random_stick_hello}"
|
||
)
|
||
return random_stick_hello
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при получении стикера: {e}")
|
||
if self.settings["Settings"]["logs"]:
|
||
await self._send_error_to_logs(
|
||
f"Отправка приветственных стикеров лажает. Ошибка: {e}"
|
||
)
|
||
return None
|
||
|
||
@track_time("send_welcome_messages", "voice_bot_service")
|
||
@track_errors("voice_bot_service", "send_welcome_messages")
|
||
async def send_welcome_messages(self, message, user_emoji: str):
|
||
"""Отправить приветственные сообщения"""
|
||
try:
|
||
# Отправляем стикер
|
||
sticker = await self.get_welcome_sticker()
|
||
if sticker:
|
||
await message.answer_sticker(sticker)
|
||
await asyncio.sleep(STICKER_DELAY)
|
||
|
||
# Отправляем приветственное сообщение
|
||
markup = self._get_main_keyboard()
|
||
await message.answer(
|
||
text="<b>Привет.</b>",
|
||
parse_mode="html",
|
||
reply_markup=markup,
|
||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||
)
|
||
await asyncio.sleep(STICKER_DELAY)
|
||
|
||
# Отправляем описание
|
||
await message.answer(
|
||
text="<i>Здесь можно послушать голосовые сообщения от совершенно незнакомых людей из Бийска</i>",
|
||
parse_mode="html",
|
||
reply_markup=markup,
|
||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||
)
|
||
await asyncio.sleep(MESSAGE_DELAY_1)
|
||
|
||
# Отправляем аналогию
|
||
await message.answer(
|
||
text="Это почти как написать письмо, положить его в бутылку и швырнуть в океан. Никогда не узнаешь, послушал его кто-то или нет и ответить тоже не получится..",
|
||
parse_mode="html",
|
||
reply_markup=markup,
|
||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||
)
|
||
await asyncio.sleep(MESSAGE_DELAY_2)
|
||
|
||
# Отправляем правила
|
||
await message.answer(
|
||
text="Записывать можно всё что угодно — никаких правил нет. Главное — твой голос, <i>хотя бы на 5-10 секунд</i>",
|
||
parse_mode="html",
|
||
reply_markup=markup,
|
||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||
)
|
||
await asyncio.sleep(MESSAGE_DELAY_3)
|
||
|
||
# Отправляем информацию об анонимности
|
||
await message.answer(
|
||
text="Здесь всё анонимно: тот, кому я отправлю твое сообщение, не узнает ни твое имя, ни твой аккаунт (так что можно не стесняться говорить то, что не стал(а) бы выкладывать в собственные соцсети)",
|
||
parse_mode="html",
|
||
reply_markup=markup,
|
||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||
)
|
||
await asyncio.sleep(MESSAGE_DELAY_4)
|
||
|
||
# Отправляем предложения
|
||
await message.answer(
|
||
text="Если не знаешь, что сказать, можешь просто прочитать любое текстовое сообщение из недавно полученных или отправленных (или спеть, рассказать стихотворенье)",
|
||
parse_mode="html",
|
||
reply_markup=markup,
|
||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||
)
|
||
await asyncio.sleep(MESSAGE_DELAY_4)
|
||
|
||
# Отправляем информацию об эмодзи
|
||
await message.answer(
|
||
text=f"Любые войсы будут помечены эмоджи. <b>Твой эмоджи - </b>{user_emoji}Таким эмоджи будут помечены твои сообщения для других Но другие люди не узнают кто за каким эмоджи скрывается:)",
|
||
parse_mode="html",
|
||
reply_markup=markup,
|
||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||
)
|
||
await asyncio.sleep(MESSAGE_DELAY_4)
|
||
|
||
# Отправляем информацию о помощи
|
||
await message.answer(
|
||
text="Так же можешь ознакомиться с инструкцией к боту по команде /help",
|
||
parse_mode="html",
|
||
reply_markup=markup,
|
||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||
)
|
||
await asyncio.sleep(MESSAGE_DELAY_4)
|
||
|
||
# Отправляем финальное сообщение
|
||
await message.answer(
|
||
text="<b>Ну всё, достаточно инструкций. записывайся! Микрофон твой - </b> 🎤",
|
||
parse_mode="html",
|
||
reply_markup=markup,
|
||
disable_web_page_preview=not self.settings["Telegram"]["preview_link"],
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при отправке приветственных сообщений: {e}")
|
||
raise VoiceMessageError(
|
||
f"Не удалось отправить приветственные сообщения: {e}"
|
||
)
|
||
|
||
@track_time("get_random_audio", "voice_bot_service")
|
||
@track_errors("voice_bot_service", "get_random_audio")
|
||
async def get_random_audio(self, user_id: int) -> Optional[Tuple[str, str, str]]:
|
||
"""Получить случайное аудио для прослушивания"""
|
||
try:
|
||
check_audio = await self.bot_db.check_listen_audio(user_id=user_id)
|
||
list_audio = list(check_audio)
|
||
|
||
if not list_audio:
|
||
return None
|
||
|
||
# Получаем случайное аудио
|
||
number_element = random.randint(0, len(list_audio) - 1)
|
||
audio_for_user = check_audio[number_element]
|
||
|
||
# Получаем информацию об авторе
|
||
user_id_author = await self.bot_db.get_user_id_by_file_name(audio_for_user)
|
||
date_added = await self.bot_db.get_date_by_file_name(audio_for_user)
|
||
user_emoji = await self.bot_db.get_user_emoji(user_id_author)
|
||
|
||
return audio_for_user, date_added, user_emoji
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при получении случайного аудио: {e}")
|
||
raise AudioProcessingError(f"Не удалось получить случайное аудио: {e}")
|
||
|
||
@track_time("mark_audio_as_listened", "voice_bot_service")
|
||
@track_errors("voice_bot_service", "mark_audio_as_listened")
|
||
async def mark_audio_as_listened(self, file_name: str, user_id: int) -> None:
|
||
"""Пометить аудио как прослушанное"""
|
||
try:
|
||
await self.bot_db.mark_listened_audio(file_name, user_id=user_id)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при пометке аудио как прослушанного: {e}")
|
||
raise DatabaseError(f"Не удалось пометить аудио как прослушанное: {e}")
|
||
|
||
@track_time("clear_user_listenings", "voice_bot_service")
|
||
@track_errors("voice_bot_service", "clear_user_listenings")
|
||
@db_query_time("clear_user_listenings", "audio_moderate", "delete")
|
||
async def clear_user_listenings(self, user_id: int) -> None:
|
||
"""Очистить прослушивания пользователя"""
|
||
try:
|
||
await self.bot_db.delete_listen_count_for_user(user_id)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при очистке прослушиваний: {e}")
|
||
raise DatabaseError(f"Не удалось очистить прослушивания: {e}")
|
||
|
||
@track_time("get_remaining_audio_count", "voice_bot_service")
|
||
@track_errors("voice_bot_service", "get_remaining_audio_count")
|
||
async def get_remaining_audio_count(self, user_id: int) -> int:
|
||
"""Получить количество оставшихся непрослушанных аудио"""
|
||
try:
|
||
check_audio = await self.bot_db.check_listen_audio(user_id=user_id)
|
||
return len(list(check_audio))
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при получении количества аудио: {e}")
|
||
raise DatabaseError(f"Не удалось получить количество аудио: {e}")
|
||
|
||
@track_time("get_main_keyboard", "voice_bot_service")
|
||
@track_errors("voice_bot_service", "get_main_keyboard")
|
||
def _get_main_keyboard(self):
|
||
"""Получить основную клавиатуру"""
|
||
from helper_bot.keyboards.keyboards import get_main_keyboard
|
||
|
||
return get_main_keyboard()
|
||
|
||
@track_time("send_error_to_logs", "voice_bot_service")
|
||
@track_errors("voice_bot_service", "send_error_to_logs")
|
||
async def _send_error_to_logs(self, message: str) -> None:
|
||
"""Отправить ошибку в логи"""
|
||
try:
|
||
from helper_bot.utils.helper_func import send_voice_message
|
||
|
||
await send_voice_message(
|
||
self.settings["Telegram"]["important_logs"], None, None, None
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Не удалось отправить ошибку в логи: {e}")
|
||
|
||
|
||
class AudioFileService:
|
||
"""Сервис для работы с аудио файлами"""
|
||
|
||
def __init__(self, bot_db):
|
||
self.bot_db = bot_db
|
||
|
||
@track_time("generate_file_name", "audio_file_service")
|
||
@track_errors("audio_file_service", "generate_file_name")
|
||
async def generate_file_name(self, user_id: int) -> str:
|
||
"""Сгенерировать имя файла для аудио"""
|
||
try:
|
||
# Проверяем есть ли запись о файле в базе данных
|
||
user_audio_count = await self.bot_db.get_user_audio_records_count(
|
||
user_id=user_id
|
||
)
|
||
|
||
if user_audio_count == 0:
|
||
# Если нет, то генерируем имя файла
|
||
file_name = f"message_from_{user_id}_number_1"
|
||
else:
|
||
# Иначе берем последнюю запись из БД, добавляем к ней 1
|
||
file_name = await self.bot_db.get_path_for_audio_record(user_id=user_id)
|
||
if file_name:
|
||
# Извлекаем номер из имени файла и увеличиваем на 1
|
||
try:
|
||
current_number = int(file_name.split("_")[-1])
|
||
new_number = current_number + 1
|
||
except (ValueError, IndexError):
|
||
new_number = user_audio_count + 1
|
||
else:
|
||
new_number = user_audio_count + 1
|
||
|
||
file_name = f"message_from_{user_id}_number_{new_number}"
|
||
|
||
return file_name
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при генерации имени файла: {e}")
|
||
raise FileOperationError(f"Не удалось сгенерировать имя файла: {e}")
|
||
|
||
@track_time("save_audio_file", "audio_file_service")
|
||
@track_errors("audio_file_service", "save_audio_file")
|
||
async def save_audio_file(
|
||
self, file_name: str, user_id: int, date_added: datetime, file_id: str
|
||
) -> None:
|
||
"""Сохранить информацию об аудио файле в базу данных"""
|
||
try:
|
||
# Проверяем существование файла перед сохранением в БД
|
||
if not await self.verify_file_exists(file_name):
|
||
error_msg = f"Файл {file_name} не существует или поврежден, отменяем сохранение в БД"
|
||
logger.error(error_msg)
|
||
raise FileOperationError(error_msg)
|
||
|
||
await self.bot_db.add_audio_record_simple(file_name, user_id, date_added)
|
||
logger.info(
|
||
f"Информация об аудио файле успешно сохранена в БД: {file_name}"
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при сохранении аудио файла в БД: {e}")
|
||
raise DatabaseError(f"Не удалось сохранить аудио файл в БД: {e}")
|
||
|
||
@track_time("save_audio_file_with_transaction", "audio_file_service")
|
||
@track_errors("audio_file_service", "save_audio_file_with_transaction")
|
||
async def save_audio_file_with_transaction(
|
||
self, file_name: str, user_id: int, date_added: datetime, file_id: str
|
||
) -> None:
|
||
"""Сохранить информацию об аудио файле в базу данных с транзакцией"""
|
||
try:
|
||
# Проверяем существование файла перед сохранением в БД
|
||
if not await self.verify_file_exists(file_name):
|
||
error_msg = f"Файл {file_name} не существует или поврежден, отменяем сохранение в БД"
|
||
logger.error(error_msg)
|
||
raise FileOperationError(error_msg)
|
||
|
||
# Используем транзакцию для атомарности операции
|
||
await self.bot_db.add_audio_record_simple(file_name, user_id, date_added)
|
||
logger.info(
|
||
f"Информация об аудио файле успешно сохранена в БД с транзакцией: {file_name}"
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при сохранении аудио файла в БД с транзакцией: {e}")
|
||
raise DatabaseError(
|
||
f"Не удалось сохранить аудио файл в БД с транзакцией: {e}"
|
||
)
|
||
|
||
@track_time("download_and_save_audio", "audio_file_service")
|
||
@track_errors("audio_file_service", "download_and_save_audio")
|
||
async def download_and_save_audio(
|
||
self, bot, message, file_name: str, max_retries: int = 3
|
||
) -> None:
|
||
"""Скачать и сохранить аудио файл с retry механизмом"""
|
||
last_exception = None
|
||
|
||
for attempt in range(max_retries):
|
||
try:
|
||
logger.info(
|
||
f"Попытка {attempt + 1}/{max_retries} скачивания и сохранения аудио: {file_name}"
|
||
)
|
||
|
||
# Проверяем наличие голосового сообщения
|
||
if not message or not message.voice:
|
||
error_msg = "Сообщение или голосовое сообщение не найдено"
|
||
logger.error(error_msg)
|
||
raise FileOperationError(error_msg)
|
||
|
||
file_id = message.voice.file_id
|
||
logger.info(f"Получен file_id: {file_id}")
|
||
|
||
# Получаем информацию о файле
|
||
try:
|
||
file_info = await bot.get_file(file_id=file_id)
|
||
logger.info(f"Получена информация о файле: {file_info.file_path}")
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при получении информации о файле: {e}")
|
||
raise FileOperationError(
|
||
f"Не удалось получить информацию о файле: {e}"
|
||
)
|
||
|
||
# Скачиваем файл
|
||
try:
|
||
downloaded_file = await bot.download_file(
|
||
file_path=file_info.file_path
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при скачивании файла: {e}")
|
||
raise FileOperationError(f"Не удалось скачать файл: {e}")
|
||
|
||
# Проверяем что файл успешно скачан
|
||
if not downloaded_file:
|
||
error_msg = "Не удалось скачать файл - получен пустой объект"
|
||
logger.error(error_msg)
|
||
raise FileOperationError(error_msg)
|
||
|
||
# Получаем размер файла без изменения позиции
|
||
current_pos = downloaded_file.tell()
|
||
downloaded_file.seek(0, 2) # Переходим в конец файла
|
||
file_size = downloaded_file.tell()
|
||
downloaded_file.seek(current_pos) # Возвращаемся в исходную позицию
|
||
|
||
logger.info(f"Файл скачан, размер: {file_size} bytes")
|
||
|
||
# Проверяем минимальный размер файла
|
||
if file_size < 100: # Минимальный размер для аудио файла
|
||
error_msg = f"Файл слишком маленький: {file_size} bytes"
|
||
logger.error(error_msg)
|
||
raise FileOperationError(error_msg)
|
||
|
||
# Создаем директорию если она не существует
|
||
try:
|
||
os.makedirs(VOICE_USERS_DIR, exist_ok=True)
|
||
logger.info(f"Директория {VOICE_USERS_DIR} создана/проверена")
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при создании директории: {e}")
|
||
raise FileOperationError(f"Не удалось создать директорию: {e}")
|
||
|
||
file_path = f"{VOICE_USERS_DIR}/{file_name}.ogg"
|
||
logger.info(f"Сохраняем файл по пути: {file_path}")
|
||
|
||
# Сбрасываем позицию в файле перед сохранением
|
||
downloaded_file.seek(0)
|
||
|
||
# Сохраняем файл
|
||
try:
|
||
with open(file_path, "wb") as new_file:
|
||
new_file.write(downloaded_file.read())
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при записи файла на диск: {e}")
|
||
raise FileOperationError(f"Не удалось записать файл на диск: {e}")
|
||
|
||
# Проверяем что файл действительно создался и имеет правильный размер
|
||
if not os.path.exists(file_path):
|
||
error_msg = f"Файл не был создан: {file_path}"
|
||
logger.error(error_msg)
|
||
raise FileOperationError(error_msg)
|
||
|
||
saved_file_size = os.path.getsize(file_path)
|
||
if saved_file_size != file_size:
|
||
error_msg = f"Размер сохраненного файла не совпадает: ожидалось {file_size}, получено {saved_file_size}"
|
||
logger.error(error_msg)
|
||
# Удаляем поврежденный файл
|
||
try:
|
||
os.remove(file_path)
|
||
except:
|
||
pass
|
||
raise FileOperationError(error_msg)
|
||
|
||
logger.info(
|
||
f"Файл успешно сохранен: {file_path}, размер: {saved_file_size} bytes"
|
||
)
|
||
return # Успешное завершение
|
||
|
||
except Exception as e:
|
||
last_exception = e
|
||
logger.error(f"Попытка {attempt + 1}/{max_retries} неудачна: {e}")
|
||
|
||
if attempt < max_retries - 1:
|
||
wait_time = (
|
||
attempt + 1
|
||
) * 2 # Экспоненциальная задержка: 2, 4, 6 секунд
|
||
logger.info(
|
||
f"Ожидание {wait_time} секунд перед следующей попыткой..."
|
||
)
|
||
await asyncio.sleep(wait_time)
|
||
else:
|
||
logger.error(f"Все {max_retries} попыток скачивания неудачны")
|
||
logger.error(
|
||
f"Traceback последней ошибки: {traceback.format_exc()}"
|
||
)
|
||
|
||
# Если все попытки неудачны
|
||
raise FileOperationError(
|
||
f"Не удалось скачать и сохранить аудио после {max_retries} попыток. Последняя ошибка: {last_exception}"
|
||
)
|
||
|
||
@track_time("verify_file_exists", "audio_file_service")
|
||
@track_errors("audio_file_service", "verify_file_exists")
|
||
async def verify_file_exists(self, file_name: str) -> bool:
|
||
"""Проверить существование и валидность файла"""
|
||
try:
|
||
file_path = f"{VOICE_USERS_DIR}/{file_name}.ogg"
|
||
|
||
if not os.path.exists(file_path):
|
||
logger.warning(f"Файл не существует: {file_path}")
|
||
return False
|
||
|
||
file_size = os.path.getsize(file_path)
|
||
if file_size == 0:
|
||
logger.warning(f"Файл пустой: {file_path}")
|
||
return False
|
||
|
||
if file_size < 100: # Минимальный размер для аудио файла
|
||
logger.warning(
|
||
f"Файл слишком маленький: {file_path}, размер: {file_size} bytes"
|
||
)
|
||
return False
|
||
|
||
logger.info(
|
||
f"Файл проверен и валиден: {file_path}, размер: {file_size} bytes"
|
||
)
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при проверке файла {file_name}: {e}")
|
||
return False
|