Implement audio record management features in AsyncBotDB and AudioRepository
- Added methods to delete audio moderation records and retrieve all audio records in async_db.py. - Enhanced AudioRepository with functionality to delete audio records by file name and retrieve all audio message records. - Improved logging for audio record operations to enhance monitoring and debugging capabilities. - Updated related handlers to ensure proper integration of new audio management features.
This commit is contained in:
@@ -27,9 +27,9 @@ from logs.custom_logger import logger
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors
|
||||
track_errors,
|
||||
db_query_time
|
||||
)
|
||||
|
||||
# Создаем роутер с middleware для проверки доступа
|
||||
@@ -94,6 +94,7 @@ async def cancel_ban_process(
|
||||
)
|
||||
@track_time("get_last_users", "admin_handlers")
|
||||
@track_errors("admin_handlers", "get_last_users")
|
||||
@db_query_time("get_last_users", "users", "select")
|
||||
async def get_last_users(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
@@ -127,6 +128,7 @@ async def get_last_users(
|
||||
)
|
||||
@track_time("get_banned_users", "admin_handlers")
|
||||
@track_errors("admin_handlers", "get_banned_users")
|
||||
@db_query_time("get_banned_users", "users", "select")
|
||||
async def get_banned_users(
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
|
||||
272
helper_bot/handlers/admin/rate_limit_handlers.py
Normal file
272
helper_bot/handlers/admin/rate_limit_handlers.py
Normal file
@@ -0,0 +1,272 @@
|
||||
"""
|
||||
Обработчики команд для мониторинга rate limiting
|
||||
"""
|
||||
from aiogram import Router, types, F
|
||||
from aiogram.filters import Command, MagicData
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import FSInputFile
|
||||
|
||||
from helper_bot.filters.main import ChatTypeFilter
|
||||
from helper_bot.middlewares.dependencies_middleware import DependenciesMiddleware
|
||||
from helper_bot.utils.rate_limit_monitor import rate_limit_monitor, get_rate_limit_summary
|
||||
from helper_bot.utils.rate_limit_metrics import update_rate_limit_gauges, get_rate_limit_metrics_summary
|
||||
from logs.custom_logger import logger
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
track_time,
|
||||
track_errors
|
||||
)
|
||||
|
||||
|
||||
class RateLimitHandlers:
|
||||
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())
|
||||
|
||||
def _setup_handlers(self):
|
||||
# Команда для просмотра статистики rate limiting
|
||||
self.router.message.register(
|
||||
self.rate_limit_stats_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("ratelimit_stats")
|
||||
)
|
||||
|
||||
# Команда для сброса статистики rate limiting
|
||||
self.router.message.register(
|
||||
self.reset_rate_limit_stats_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("reset_ratelimit_stats")
|
||||
)
|
||||
|
||||
# Команда для просмотра ошибок rate limiting
|
||||
self.router.message.register(
|
||||
self.rate_limit_errors_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("ratelimit_errors")
|
||||
)
|
||||
|
||||
# Команда для просмотра Prometheus метрик
|
||||
self.router.message.register(
|
||||
self.rate_limit_prometheus_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
Command("ratelimit_prometheus")
|
||||
)
|
||||
|
||||
@track_time("rate_limit_stats_handler", "rate_limit_handlers")
|
||||
@track_errors("rate_limit_handlers", "rate_limit_stats_handler")
|
||||
async def rate_limit_stats_handler(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
"""Показывает статистику rate limiting"""
|
||||
try:
|
||||
# Проверяем права администратора
|
||||
if not await bot_db.is_admin(message.from_user.id):
|
||||
await message.answer("У вас нет прав для выполнения этой команды.")
|
||||
return
|
||||
|
||||
# Получаем сводку
|
||||
summary = get_rate_limit_summary()
|
||||
global_stats = rate_limit_monitor.get_global_stats()
|
||||
|
||||
# Формируем сообщение со статистикой
|
||||
stats_text = (
|
||||
f"📊 <b>Статистика Rate Limiting</b>\n\n"
|
||||
f"🔢 <b>Общая статистика:</b>\n"
|
||||
f"• Всего запросов: {summary['total_requests']}\n"
|
||||
f"• Процент успеха: {summary['success_rate']:.1%}\n"
|
||||
f"• Процент ошибок: {summary['error_rate']:.1%}\n"
|
||||
f"• Запросов в минуту: {summary['requests_per_minute']:.1f}\n"
|
||||
f"• Среднее время ожидания: {summary['average_wait_time']:.2f}с\n"
|
||||
f"• Активных чатов: {summary['active_chats']}\n"
|
||||
f"• Ошибок за час: {summary['recent_errors_count']}\n\n"
|
||||
)
|
||||
|
||||
# Добавляем детальную статистику
|
||||
stats_text += f"🔍 <b>Детальная статистика:</b>\n"
|
||||
stats_text += f"• Успешных запросов: {global_stats.successful_requests}\n"
|
||||
stats_text += f"• Неудачных запросов: {global_stats.failed_requests}\n"
|
||||
stats_text += f"• RetryAfter ошибок: {global_stats.retry_after_errors}\n"
|
||||
stats_text += f"• Других ошибок: {global_stats.other_errors}\n"
|
||||
stats_text += f"• Общее время ожидания: {global_stats.total_wait_time:.2f}с\n\n"
|
||||
|
||||
# Добавляем топ чатов по запросам
|
||||
top_chats = rate_limit_monitor.get_top_chats_by_requests(5)
|
||||
if top_chats:
|
||||
stats_text += f"📈 <b>Топ-5 чатов по запросам:</b>\n"
|
||||
for i, (chat_id, chat_stats) in enumerate(top_chats, 1):
|
||||
stats_text += f"{i}. Chat {chat_id}: {chat_stats.total_requests} запросов ({chat_stats.success_rate:.1%} успех)\n"
|
||||
stats_text += "\n"
|
||||
|
||||
# Добавляем чаты с высоким процентом ошибок
|
||||
high_error_chats = rate_limit_monitor.get_chats_with_high_error_rate(0.1)
|
||||
if high_error_chats:
|
||||
stats_text += f"⚠️ <b>Чаты с высоким процентом ошибок (>10%):</b>\n"
|
||||
for chat_id, chat_stats in high_error_chats[:3]:
|
||||
stats_text += f"• Chat {chat_id}: {chat_stats.error_rate:.1%} ошибок ({chat_stats.failed_requests}/{chat_stats.total_requests})\n"
|
||||
|
||||
await message.answer(stats_text, parse_mode='HTML')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении статистики rate limiting: {e}")
|
||||
await message.answer("Произошла ошибка при получении статистики.")
|
||||
|
||||
@track_time("reset_rate_limit_stats_handler", "rate_limit_handlers")
|
||||
@track_errors("rate_limit_handlers", "reset_rate_limit_stats_handler")
|
||||
async def reset_rate_limit_stats_handler(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
"""Сбрасывает статистику rate limiting"""
|
||||
try:
|
||||
# Проверяем права администратора
|
||||
if not await bot_db.is_admin(message.from_user.id):
|
||||
await message.answer("У вас нет прав для выполнения этой команды.")
|
||||
return
|
||||
|
||||
# Сбрасываем статистику
|
||||
rate_limit_monitor.reset_stats()
|
||||
|
||||
await message.answer("✅ Статистика rate limiting сброшена.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сбросе статистики rate limiting: {e}")
|
||||
await message.answer("Произошла ошибка при сбросе статистики.")
|
||||
|
||||
@track_time("rate_limit_errors_handler", "rate_limit_handlers")
|
||||
@track_errors("rate_limit_handlers", "rate_limit_errors_handler")
|
||||
async def rate_limit_errors_handler(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
"""Показывает недавние ошибки rate limiting"""
|
||||
try:
|
||||
# Проверяем права администратора
|
||||
if not await bot_db.is_admin(message.from_user.id):
|
||||
await message.answer("У вас нет прав для выполнения этой команды.")
|
||||
return
|
||||
|
||||
# Получаем ошибки за последний час
|
||||
recent_errors = rate_limit_monitor.get_recent_errors(60)
|
||||
error_summary = rate_limit_monitor.get_error_summary(60)
|
||||
|
||||
if not recent_errors:
|
||||
await message.answer("✅ Ошибок rate limiting за последний час не было.")
|
||||
return
|
||||
|
||||
# Формируем сообщение с ошибками
|
||||
errors_text = f"🚨 <b>Ошибки Rate Limiting (последний час)</b>\n\n"
|
||||
errors_text += f"📊 <b>Сводка ошибок:</b>\n"
|
||||
for error_type, count in error_summary.items():
|
||||
errors_text += f"• {error_type}: {count}\n"
|
||||
errors_text += f"\nВсего ошибок: {len(recent_errors)}\n\n"
|
||||
|
||||
# Показываем последние 10 ошибок
|
||||
errors_text += f"🔍 <b>Последние ошибки:</b>\n"
|
||||
for i, error in enumerate(recent_errors[-10:], 1):
|
||||
from datetime import datetime
|
||||
timestamp = datetime.fromtimestamp(error['timestamp']).strftime("%H:%M:%S")
|
||||
errors_text += f"{i}. {timestamp} - Chat {error['chat_id']} - {error['error_type']}\n"
|
||||
|
||||
# Если сообщение слишком длинное, разбиваем на части
|
||||
if len(errors_text) > 4000:
|
||||
# Отправляем сводку
|
||||
summary_text = f"🚨 <b>Ошибки Rate Limiting (последний час)</b>\n\n"
|
||||
summary_text += f"📊 <b>Сводка ошибок:</b>\n"
|
||||
for error_type, count in error_summary.items():
|
||||
summary_text += f"• {error_type}: {count}\n"
|
||||
summary_text += f"\nВсего ошибок: {len(recent_errors)}"
|
||||
|
||||
await message.answer(summary_text, parse_mode='HTML')
|
||||
|
||||
# Отправляем детали отдельным сообщением
|
||||
details_text = f"🔍 <b>Последние ошибки:</b>\n"
|
||||
for i, error in enumerate(recent_errors[-10:], 1):
|
||||
from datetime import datetime
|
||||
timestamp = datetime.fromtimestamp(error['timestamp']).strftime("%H:%M:%S")
|
||||
details_text += f"{i}. {timestamp} - Chat {error['chat_id']} - {error['error_type']}\n"
|
||||
|
||||
await message.answer(details_text, parse_mode='HTML')
|
||||
else:
|
||||
await message.answer(errors_text, parse_mode='HTML')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении ошибок rate limiting: {e}")
|
||||
await message.answer("Произошла ошибка при получении информации об ошибках.")
|
||||
|
||||
@track_time("rate_limit_prometheus_handler", "rate_limit_handlers")
|
||||
@track_errors("rate_limit_handlers", "rate_limit_prometheus_handler")
|
||||
async def rate_limit_prometheus_handler(
|
||||
self,
|
||||
message: types.Message,
|
||||
state: FSMContext,
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
"""Показывает Prometheus метрики rate limiting"""
|
||||
try:
|
||||
# Проверяем права администратора
|
||||
if not await bot_db.is_admin(message.from_user.id):
|
||||
await message.answer("У вас нет прав для выполнения этой команды.")
|
||||
return
|
||||
|
||||
# Обновляем gauge метрики
|
||||
update_rate_limit_gauges()
|
||||
|
||||
# Получаем сводку метрик
|
||||
metrics_summary = get_rate_limit_metrics_summary()
|
||||
|
||||
# Формируем сообщение с метриками
|
||||
metrics_text = (
|
||||
f"📊 <b>Prometheus метрики Rate Limiting</b>\n\n"
|
||||
f"🔢 <b>Основные метрики:</b>\n"
|
||||
f"• rate_limit_requests_total: {metrics_summary['total_requests']}\n"
|
||||
f"• rate_limit_success_rate: {metrics_summary['success_rate']:.3f}\n"
|
||||
f"• rate_limit_error_rate: {metrics_summary['error_rate']:.3f}\n"
|
||||
f"• rate_limit_requests_per_minute: {metrics_summary['requests_per_minute']:.1f}\n"
|
||||
f"• rate_limit_avg_wait_time: {metrics_summary['average_wait_time']:.3f}s\n"
|
||||
f"• rate_limit_active_chats: {metrics_summary['active_chats']}\n\n"
|
||||
)
|
||||
|
||||
# Добавляем детальные метрики
|
||||
metrics_text += f"🔍 <b>Детальные метрики:</b>\n"
|
||||
metrics_text += f"• Успешных запросов: {metrics_summary['successful_requests']}\n"
|
||||
metrics_text += f"• Неудачных запросов: {metrics_summary['failed_requests']}\n"
|
||||
metrics_text += f"• RetryAfter ошибок: {metrics_summary['retry_after_errors']}\n"
|
||||
metrics_text += f"• Других ошибок: {metrics_summary['other_errors']}\n"
|
||||
metrics_text += f"• Общее время ожидания: {metrics_summary['total_wait_time']:.2f}s\n\n"
|
||||
|
||||
# Добавляем информацию о доступных метриках
|
||||
metrics_text += f"📈 <b>Доступные Prometheus метрики:</b>\n"
|
||||
metrics_text += f"• rate_limit_requests_total - общее количество запросов\n"
|
||||
metrics_text += f"• rate_limit_errors_total - количество ошибок по типам\n"
|
||||
metrics_text += f"• rate_limit_wait_duration_seconds - время ожидания\n"
|
||||
metrics_text += f"• rate_limit_request_interval_seconds - интервалы между запросами\n"
|
||||
metrics_text += f"• rate_limit_active_chats - количество активных чатов\n"
|
||||
metrics_text += f"• rate_limit_success_rate - процент успеха по чатам\n"
|
||||
metrics_text += f"• rate_limit_requests_per_minute - запросов в минуту\n"
|
||||
metrics_text += f"• rate_limit_total_requests - общее количество запросов\n"
|
||||
metrics_text += f"• rate_limit_total_errors - количество ошибок\n"
|
||||
metrics_text += f"• rate_limit_avg_wait_time - среднее время ожидания\n"
|
||||
|
||||
await message.answer(metrics_text, parse_mode='HTML')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении Prometheus метрик: {e}")
|
||||
await message.answer("Произошла ошибка при получении метрик.")
|
||||
@@ -7,10 +7,8 @@ from logs.custom_logger import logger
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors,
|
||||
db_query_time
|
||||
track_errors
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ from logs.custom_logger import logger
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors,
|
||||
db_query_time
|
||||
db_query_time,
|
||||
track_file_operations
|
||||
)
|
||||
|
||||
callback_router = Router()
|
||||
@@ -238,6 +238,8 @@ async def change_page(
|
||||
@callback_router.callback_query(F.data == CALLBACK_SAVE)
|
||||
@track_time("save_voice_message", "callback_handlers")
|
||||
@track_errors("callback_handlers", "save_voice_message")
|
||||
@track_file_operations("voice")
|
||||
@db_query_time("save_voice_message", "audio_moderate", "mixed")
|
||||
async def save_voice_message(
|
||||
call: CallbackQuery,
|
||||
bot_db: MagicData("bot_db"),
|
||||
@@ -245,14 +247,18 @@ async def save_voice_message(
|
||||
**kwargs
|
||||
):
|
||||
try:
|
||||
logger.info(f"Начинаем сохранение голосового сообщения. Message ID: {call.message.message_id}")
|
||||
|
||||
# Создаем сервис для работы с аудио файлами
|
||||
audio_service = AudioFileService(bot_db)
|
||||
|
||||
# Получаем ID пользователя из базы
|
||||
user_id = await bot_db.get_user_id_by_message_id_for_voice_bot(call.message.message_id)
|
||||
logger.info(f"Получен user_id: {user_id}")
|
||||
|
||||
# Генерируем имя файла
|
||||
file_name = await audio_service.generate_file_name(user_id)
|
||||
logger.info(f"Сгенерировано имя файла: {file_name}")
|
||||
|
||||
# Собираем инфо о сообщении
|
||||
time_UTC = int(time.time())
|
||||
@@ -260,32 +266,54 @@ async def save_voice_message(
|
||||
|
||||
# Получаем file_id из voice сообщения
|
||||
file_id = call.message.voice.file_id if call.message.voice else ""
|
||||
logger.info(f"Получен file_id: {file_id}")
|
||||
|
||||
# Сохраняем в базу данных
|
||||
await audio_service.save_audio_file(file_name, user_id, date_added, file_id)
|
||||
|
||||
# Скачиваем и сохраняем файл
|
||||
# ВАЖНО: Сначала скачиваем и сохраняем файл на диск
|
||||
logger.info("Начинаем скачивание и сохранение файла на диск...")
|
||||
await audio_service.download_and_save_audio(call.bot, call.message, file_name)
|
||||
logger.info("Файл успешно скачан и сохранен на диск")
|
||||
|
||||
# Только после успешного сохранения файла - сохраняем в базу данных
|
||||
logger.info("Начинаем сохранение информации в базу данных...")
|
||||
await audio_service.save_audio_file(file_name, user_id, date_added, file_id)
|
||||
logger.info("Информация успешно сохранена в базу данных")
|
||||
|
||||
# Удаляем сообщение из предложки
|
||||
logger.info("Удаляем сообщение из предложки...")
|
||||
await call.bot.delete_message(
|
||||
chat_id=settings['Telegram']['group_for_posts'],
|
||||
message_id=call.message.message_id
|
||||
)
|
||||
logger.info("Сообщение удалено из предложки")
|
||||
|
||||
# Удаляем запись из таблицы audio_moderate
|
||||
logger.info("Удаляем запись из таблицы audio_moderate...")
|
||||
await bot_db.delete_audio_moderate_record(call.message.message_id)
|
||||
logger.info("Запись удалена из таблицы audio_moderate")
|
||||
|
||||
await call.answer(text='Сохранено!', cache_time=3)
|
||||
logger.info(f"Голосовое сообщение успешно сохранено: {file_name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при сохранении голосового сообщения: {e}")
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
|
||||
# Дополнительная информация для диагностики
|
||||
try:
|
||||
if 'call' in locals() and call.message:
|
||||
logger.error(f"Message ID: {call.message.message_id}")
|
||||
logger.error(f"User ID: {user_id if 'user_id' in locals() else 'не определен'}")
|
||||
logger.error(f"File name: {file_name if 'file_name' in locals() else 'не определен'}")
|
||||
except:
|
||||
pass
|
||||
|
||||
await call.answer(text='Ошибка при сохранении!', cache_time=3)
|
||||
|
||||
|
||||
@callback_router.callback_query(F.data == CALLBACK_DELETE)
|
||||
@track_time("delete_voice_message", "callback_handlers")
|
||||
@track_errors("callback_handlers", "delete_voice_message")
|
||||
@db_query_time("delete_voice_message", "audio_moderate", "delete")
|
||||
async def delete_voice_message(
|
||||
call: CallbackQuery,
|
||||
bot_db: MagicData("bot_db"),
|
||||
|
||||
@@ -25,7 +25,7 @@ from logs.custom_logger import logger
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_media_processing,
|
||||
track_time,
|
||||
track_errors,
|
||||
db_query_time
|
||||
@@ -140,6 +140,7 @@ class PostPublishService:
|
||||
|
||||
@track_time("_publish_media_group", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_publish_media_group")
|
||||
@track_media_processing("media_group")
|
||||
async def _publish_media_group(self, call: CallbackQuery) -> None:
|
||||
"""Публикация медиагруппы"""
|
||||
logger.info(f"Начинаю публикацию медиагруппы. Helper message ID: {call.message.message_id}")
|
||||
@@ -230,6 +231,7 @@ class PostPublishService:
|
||||
|
||||
@track_time("_decline_media_group", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_decline_media_group")
|
||||
@track_media_processing("media_group")
|
||||
async def _decline_media_group(self, call: CallbackQuery) -> None:
|
||||
"""Отклонение медиагруппы"""
|
||||
logger.debug(f"Отклоняю медиагруппу. Helper message ID: {call.message.message_id}")
|
||||
@@ -308,6 +310,7 @@ class PostPublishService:
|
||||
|
||||
@track_time("_delete_media_group_and_notify_author", "post_publish_service")
|
||||
@track_errors("post_publish_service", "_delete_media_group_and_notify_author")
|
||||
@track_media_processing("media_group")
|
||||
async def _delete_media_group_and_notify_author(self, call: CallbackQuery, author_id: int) -> None:
|
||||
"""Удаление медиагруппы и уведомление автора"""
|
||||
post_ids = await self.db.get_post_ids_from_telegram_by_last_id(call.message.message_id)
|
||||
@@ -339,6 +342,7 @@ class BanService:
|
||||
|
||||
@track_time("ban_user_from_post", "ban_service")
|
||||
@track_errors("ban_service", "ban_user_from_post")
|
||||
@db_query_time("ban_user_from_post", "users", "mixed")
|
||||
async def ban_user_from_post(self, call: CallbackQuery) -> None:
|
||||
"""Бан пользователя за спам"""
|
||||
author_id = await self.db.get_author_id_by_message_id(call.message.message_id)
|
||||
@@ -379,6 +383,7 @@ class BanService:
|
||||
|
||||
@track_time("unlock_user", "ban_service")
|
||||
@track_errors("ban_service", "unlock_user")
|
||||
@db_query_time("unlock_user", "users", "delete")
|
||||
async def unlock_user(self, user_id: str) -> str:
|
||||
"""Разблокировка пользователя"""
|
||||
user_name = await self.db.get_username(int(user_id))
|
||||
|
||||
@@ -13,9 +13,8 @@ from logs.custom_logger import logger
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors,
|
||||
track_errors,
|
||||
db_query_time
|
||||
)
|
||||
|
||||
@@ -34,6 +33,7 @@ class AdminReplyService:
|
||||
|
||||
@track_time("get_user_id_for_reply", "admin_reply_service")
|
||||
@track_errors("admin_reply_service", "get_user_id_for_reply")
|
||||
@db_query_time("get_user_id_for_reply", "users", "select")
|
||||
async def get_user_id_for_reply(self, message_id: int) -> int:
|
||||
"""
|
||||
Get user ID for reply by message ID.
|
||||
|
||||
@@ -27,7 +27,6 @@ from helper_bot.utils.helper_func import (
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors,
|
||||
db_query_time
|
||||
@@ -173,6 +172,7 @@ class PrivateHandlers:
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "stickers")
|
||||
@track_time("stickers", "private_handlers")
|
||||
@db_query_time("stickers", "stickers", "update")
|
||||
async def stickers(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Handle stickers request"""
|
||||
# User service operations with metrics
|
||||
@@ -200,6 +200,7 @@ class PrivateHandlers:
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "resend_message_in_group_for_message")
|
||||
@track_time("resend_message_in_group_for_message", "private_handlers")
|
||||
@db_query_time("resend_message_in_group_for_message", "messages", "insert")
|
||||
async def resend_message_in_group_for_message(self, message: types.Message, state: FSMContext, **kwargs):
|
||||
"""Handle messages in admin chat states"""
|
||||
# User service operations with metrics
|
||||
|
||||
@@ -33,10 +33,11 @@ from helper_bot.keyboards import get_reply_keyboard_for_post
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors,
|
||||
db_query_time
|
||||
db_query_time,
|
||||
track_media_processing,
|
||||
track_file_operations
|
||||
)
|
||||
|
||||
|
||||
@@ -74,12 +75,14 @@ class UserService:
|
||||
|
||||
@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
|
||||
@@ -89,43 +92,41 @@ class UserService:
|
||||
is_bot = message.from_user.is_bot
|
||||
language_code = message.from_user.language_code
|
||||
|
||||
if not await self.db.user_exists(user_id):
|
||||
# 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,
|
||||
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
|
||||
)
|
||||
await self.db.add_user(user)
|
||||
metrics.record_db_query("add_user", 0.0, "users", "insert")
|
||||
else:
|
||||
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)
|
||||
metrics.record_db_query("update_username_fullname", 0.0, "users", "update")
|
||||
safe_full_name = html.escape(full_name) if full_name else "Неизвестный пользователь"
|
||||
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}')
|
||||
# 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,
|
||||
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 "Неизвестный пользователь"
|
||||
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)
|
||||
metrics.record_db_query("update_user_date", 0.0, "users", "update")
|
||||
|
||||
|
||||
@track_errors("user_service", "log_user_message")
|
||||
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)
|
||||
@@ -146,6 +147,7 @@ class PostService:
|
||||
|
||||
@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"""
|
||||
post_text = get_text_message(message.text.lower(), first_name, message.from_user.username)
|
||||
@@ -162,6 +164,7 @@ class PostService:
|
||||
|
||||
@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"""
|
||||
post_caption = ""
|
||||
@@ -186,6 +189,7 @@ class PostService:
|
||||
|
||||
@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"""
|
||||
post_caption = ""
|
||||
@@ -210,6 +214,7 @@ class PostService:
|
||||
|
||||
@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()
|
||||
@@ -230,6 +235,7 @@ class PostService:
|
||||
|
||||
@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"""
|
||||
post_caption = ""
|
||||
@@ -254,6 +260,7 @@ class PostService:
|
||||
|
||||
@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()
|
||||
@@ -273,7 +280,9 @@ class PostService:
|
||||
logger.warning(f"handle_photo_post: Не удалось сохранить медиа для поста {sent_message.message_id}")
|
||||
|
||||
@track_time("handle_media_group_post", "post_service")
|
||||
@track_errors("post_service", "handle_media_group_post")
|
||||
@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 = " "
|
||||
@@ -320,6 +329,7 @@ class PostService:
|
||||
|
||||
@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:
|
||||
"""Process post based on content type"""
|
||||
first_name = get_first_name(message)
|
||||
@@ -360,6 +370,7 @@ class StickerService:
|
||||
|
||||
@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_*'))
|
||||
@@ -372,6 +383,7 @@ class StickerService:
|
||||
|
||||
@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_*'))
|
||||
|
||||
191
helper_bot/handlers/voice/cleanup_utils.py
Normal file
191
helper_bot/handlers/voice/cleanup_utils.py
Normal file
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
Утилиты для очистки и диагностики проблем с голосовыми файлами
|
||||
"""
|
||||
import os
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple
|
||||
from logs.custom_logger import logger
|
||||
from helper_bot.handlers.voice.constants import VOICE_USERS_DIR
|
||||
|
||||
|
||||
class VoiceFileCleanupUtils:
|
||||
"""Утилиты для очистки и диагностики голосовых файлов"""
|
||||
|
||||
def __init__(self, bot_db):
|
||||
self.bot_db = bot_db
|
||||
|
||||
async def find_orphaned_db_records(self) -> List[Tuple[str, int]]:
|
||||
"""Найти записи в БД, для которых нет соответствующих файлов"""
|
||||
try:
|
||||
# Получаем все записи из БД
|
||||
all_audio_records = await self.bot_db.get_all_audio_records()
|
||||
orphaned_records = []
|
||||
|
||||
for record in all_audio_records:
|
||||
file_name = record.get('file_name', '')
|
||||
user_id = record.get('author_id', 0)
|
||||
|
||||
file_path = f'{VOICE_USERS_DIR}/{file_name}.ogg'
|
||||
if not os.path.exists(file_path):
|
||||
orphaned_records.append((file_name, user_id))
|
||||
logger.warning(f"Найдена запись в БД без файла: {file_name} (user_id: {user_id})")
|
||||
|
||||
logger.info(f"Найдено {len(orphaned_records)} записей в БД без соответствующих файлов")
|
||||
return orphaned_records
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при поиске orphaned записей: {e}")
|
||||
return []
|
||||
|
||||
async def find_orphaned_files(self) -> List[str]:
|
||||
"""Найти файлы на диске, для которых нет записей в БД"""
|
||||
try:
|
||||
if not os.path.exists(VOICE_USERS_DIR):
|
||||
logger.warning(f"Директория {VOICE_USERS_DIR} не существует")
|
||||
return []
|
||||
|
||||
# Получаем все файлы .ogg в директории
|
||||
ogg_files = list(Path(VOICE_USERS_DIR).glob("*.ogg"))
|
||||
orphaned_files = []
|
||||
|
||||
# Получаем все записи из БД
|
||||
all_audio_records = await self.bot_db.get_all_audio_records()
|
||||
db_file_names = {record.get('file_name', '') for record in all_audio_records}
|
||||
|
||||
for file_path in ogg_files:
|
||||
file_name = file_path.stem # Имя файла без расширения
|
||||
if file_name not in db_file_names:
|
||||
orphaned_files.append(str(file_path))
|
||||
logger.warning(f"Найден файл без записи в БД: {file_path}")
|
||||
|
||||
logger.info(f"Найдено {len(orphaned_files)} файлов без записей в БД")
|
||||
return orphaned_files
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при поиске orphaned файлов: {e}")
|
||||
return []
|
||||
|
||||
async def cleanup_orphaned_db_records(self, dry_run: bool = True) -> int:
|
||||
"""Удалить записи в БД, для которых нет файлов"""
|
||||
try:
|
||||
orphaned_records = await self.find_orphaned_db_records()
|
||||
|
||||
if not orphaned_records:
|
||||
logger.info("Нет orphaned записей для удаления")
|
||||
return 0
|
||||
|
||||
if dry_run:
|
||||
logger.info(f"DRY RUN: Найдено {len(orphaned_records)} записей для удаления")
|
||||
for file_name, user_id in orphaned_records:
|
||||
logger.info(f"DRY RUN: Будет удалена запись: {file_name} (user_id: {user_id})")
|
||||
return len(orphaned_records)
|
||||
|
||||
# Удаляем записи
|
||||
deleted_count = 0
|
||||
for file_name, user_id in orphaned_records:
|
||||
try:
|
||||
await self.bot_db.delete_audio_record_by_file_name(file_name)
|
||||
deleted_count += 1
|
||||
logger.info(f"Удалена запись в БД: {file_name} (user_id: {user_id})")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении записи {file_name}: {e}")
|
||||
|
||||
logger.info(f"Удалено {deleted_count} orphaned записей из БД")
|
||||
return deleted_count
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при очистке orphaned записей: {e}")
|
||||
return 0
|
||||
|
||||
async def cleanup_orphaned_files(self, dry_run: bool = True) -> int:
|
||||
"""Удалить файлы на диске, для которых нет записей в БД"""
|
||||
try:
|
||||
orphaned_files = await self.find_orphaned_files()
|
||||
|
||||
if not orphaned_files:
|
||||
logger.info("Нет orphaned файлов для удаления")
|
||||
return 0
|
||||
|
||||
if dry_run:
|
||||
logger.info(f"DRY RUN: Найдено {len(orphaned_files)} файлов для удаления")
|
||||
for file_path in orphaned_files:
|
||||
logger.info(f"DRY RUN: Будет удален файл: {file_path}")
|
||||
return len(orphaned_files)
|
||||
|
||||
# Удаляем файлы
|
||||
deleted_count = 0
|
||||
for file_path in orphaned_files:
|
||||
try:
|
||||
os.remove(file_path)
|
||||
deleted_count += 1
|
||||
logger.info(f"Удален файл: {file_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении файла {file_path}: {e}")
|
||||
|
||||
logger.info(f"Удалено {deleted_count} orphaned файлов")
|
||||
return deleted_count
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при очистке orphaned файлов: {e}")
|
||||
return 0
|
||||
|
||||
async def get_disk_usage_stats(self) -> dict:
|
||||
"""Получить статистику использования диска"""
|
||||
try:
|
||||
if not os.path.exists(VOICE_USERS_DIR):
|
||||
return {"error": f"Директория {VOICE_USERS_DIR} не существует"}
|
||||
|
||||
total_size = 0
|
||||
file_count = 0
|
||||
|
||||
for file_path in Path(VOICE_USERS_DIR).glob("*.ogg"):
|
||||
if file_path.is_file():
|
||||
total_size += file_path.stat().st_size
|
||||
file_count += 1
|
||||
|
||||
return {
|
||||
"total_files": file_count,
|
||||
"total_size_bytes": total_size,
|
||||
"total_size_mb": round(total_size / (1024 * 1024), 2),
|
||||
"directory": VOICE_USERS_DIR
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении статистики диска: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
async def run_full_diagnostic(self) -> dict:
|
||||
"""Запустить полную диагностику"""
|
||||
try:
|
||||
logger.info("Запуск полной диагностики голосовых файлов...")
|
||||
|
||||
# Статистика диска
|
||||
disk_stats = await self.get_disk_usage_stats()
|
||||
|
||||
# Orphaned записи в БД
|
||||
orphaned_db_records = await self.find_orphaned_db_records()
|
||||
|
||||
# Orphaned файлы
|
||||
orphaned_files = await self.find_orphaned_files()
|
||||
|
||||
# Количество записей в БД
|
||||
all_audio_records = await self.bot_db.get_all_audio_records()
|
||||
db_records_count = len(all_audio_records)
|
||||
|
||||
diagnostic_result = {
|
||||
"disk_stats": disk_stats,
|
||||
"db_records_count": db_records_count,
|
||||
"orphaned_db_records_count": len(orphaned_db_records),
|
||||
"orphaned_files_count": len(orphaned_files),
|
||||
"orphaned_db_records": orphaned_db_records[:10], # Первые 10 для примера
|
||||
"orphaned_files": orphaned_files[:10], # Первые 10 для примера
|
||||
"status": "healthy" if len(orphaned_db_records) == 0 and len(orphaned_files) == 0 else "issues_found"
|
||||
}
|
||||
|
||||
logger.info(f"Диагностика завершена. Статус: {diagnostic_result['status']}")
|
||||
return diagnostic_result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при диагностике: {e}")
|
||||
return {"error": str(e)}
|
||||
@@ -1,6 +1,7 @@
|
||||
import random
|
||||
import asyncio
|
||||
import traceback
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
@@ -16,7 +17,6 @@ from logs.custom_logger import logger
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors,
|
||||
db_query_time
|
||||
@@ -190,6 +190,7 @@ class VoiceBotService:
|
||||
|
||||
@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:
|
||||
@@ -275,62 +276,170 @@ class AudioFileService:
|
||||
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) -> None:
|
||||
"""Скачать и сохранить аудио файл"""
|
||||
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:
|
||||
logger.info(f"Начинаем скачивание и сохранение аудио: {file_name}")
|
||||
|
||||
# Проверяем наличие голосового сообщения
|
||||
if not message or not message.voice:
|
||||
logger.error("Сообщение или голосовое сообщение не найдено")
|
||||
raise FileOperationError("Сообщение или голосовое сообщение не найдено")
|
||||
|
||||
file_id = message.voice.file_id
|
||||
logger.info(f"Получен file_id: {file_id}")
|
||||
|
||||
file_info = await bot.get_file(file_id=file_id)
|
||||
logger.info(f"Получена информация о файле: {file_info.file_path}")
|
||||
|
||||
downloaded_file = await bot.download_file(file_path=file_info.file_path)
|
||||
|
||||
# Проверяем что файл успешно скачан
|
||||
if not downloaded_file:
|
||||
logger.error("Не удалось скачать файл")
|
||||
raise FileOperationError("Не удалось скачать файл")
|
||||
|
||||
# Получаем размер файла без изменения позиции
|
||||
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")
|
||||
|
||||
# Создаем директорию если она не существует
|
||||
import os
|
||||
os.makedirs(VOICE_USERS_DIR, exist_ok=True)
|
||||
logger.info(f"Директория {VOICE_USERS_DIR} создана/проверена")
|
||||
|
||||
file_path = f'{VOICE_USERS_DIR}/{file_name}.ogg'
|
||||
logger.info(f"Сохраняем файл по пути: {file_path}")
|
||||
|
||||
# Сбрасываем позицию в файле перед сохранением
|
||||
downloaded_file.seek(0)
|
||||
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
|
||||
|
||||
# Сохраняем файл
|
||||
with open(file_path, 'wb') as new_file:
|
||||
new_file.write(downloaded_file.read())
|
||||
|
||||
logger.info(f"Файл успешно сохранен: {file_path}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при скачивании и сохранении аудио: {e}")
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
raise FileOperationError(f"Не удалось скачать и сохранить аудио: {e}")
|
||||
logger.error(f"Ошибка при проверке файла {file_name}: {e}")
|
||||
return False
|
||||
|
||||
@@ -6,6 +6,11 @@ from typing import Optional
|
||||
from helper_bot.handlers.voice.exceptions import DatabaseError
|
||||
from logs.custom_logger import logger
|
||||
|
||||
from helper_bot.utils.metrics import (
|
||||
track_time,
|
||||
track_errors,
|
||||
db_query_time
|
||||
)
|
||||
|
||||
def format_time_ago(date_from_db: str) -> Optional[str]:
|
||||
"""Форматировать время с момента последней записи"""
|
||||
@@ -69,7 +74,9 @@ def plural_time(type: int, n: float) -> str:
|
||||
new_number = int(n)
|
||||
return str(new_number) + ' ' + word[p]
|
||||
|
||||
|
||||
@track_time("get_last_message_text", "voice_utils")
|
||||
@track_errors("voice_utils", "get_last_message_text")
|
||||
@db_query_time("get_last_message_text", "voice", "select")
|
||||
async def get_last_message_text(bot_db) -> Optional[str]:
|
||||
"""Получить текст сообщения о времени последней записи"""
|
||||
try:
|
||||
@@ -88,7 +95,9 @@ async def validate_voice_message(message) -> bool:
|
||||
"""Проверить валидность голосового сообщения"""
|
||||
return message.content_type == 'voice'
|
||||
|
||||
|
||||
@track_time("get_user_emoji_safe", "voice_utils")
|
||||
@track_errors("voice_utils", "get_user_emoji_safe")
|
||||
@db_query_time("get_user_emoji_safe", "voice", "select")
|
||||
async def get_user_emoji_safe(bot_db, user_id: int) -> str:
|
||||
"""Безопасно получить эмодзи пользователя"""
|
||||
try:
|
||||
|
||||
@@ -24,10 +24,10 @@ from helper_bot.handlers.private.constants import BUTTON_TEXTS
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors,
|
||||
db_query_time
|
||||
db_query_time,
|
||||
track_file_operations
|
||||
)
|
||||
|
||||
class VoiceHandlers:
|
||||
@@ -126,6 +126,7 @@ class VoiceHandlers:
|
||||
@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)
|
||||
@@ -140,7 +141,7 @@ class VoiceHandlers:
|
||||
logger.info(f"Пользователь {message.from_user.id}: вызываем start")
|
||||
await self.start(message, state, bot_db, settings)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при проверке приветственного сообщения: {e}")
|
||||
logger.error(f"Ошибка при проверке приветственного сообщения для пользователя {message.from_user.id}: {e}")
|
||||
# В случае ошибки вызываем start
|
||||
await self.start(message, state, bot_db, settings)
|
||||
|
||||
@@ -169,6 +170,7 @@ class VoiceHandlers:
|
||||
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)
|
||||
@@ -183,6 +185,7 @@ class VoiceHandlers:
|
||||
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')
|
||||
@@ -194,6 +197,7 @@ class VoiceHandlers:
|
||||
|
||||
@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,
|
||||
@@ -201,7 +205,7 @@ class VoiceHandlers:
|
||||
bot_db: MagicData("bot_db"),
|
||||
settings: MagicData("settings")
|
||||
):
|
||||
logger.info(f"Пользователь {message.from_user.id}: вызывается функция start")
|
||||
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)
|
||||
@@ -210,13 +214,14 @@ class VoiceHandlers:
|
||||
# Создаем сервис и отправляем приветственные сообщения
|
||||
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"Ошибка при отметке получения приветствия: {e}")
|
||||
logger.error(f"Ошибка при отметке получения приветствия для пользователя {message.from_user.id}: {e}")
|
||||
|
||||
@track_time("cancel_handler", "voice_handlers")
|
||||
@track_errors("voice_handlers", "cancel_handler")
|
||||
@@ -233,6 +238,7 @@ class VoiceHandlers:
|
||||
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")
|
||||
@@ -243,6 +249,7 @@ class VoiceHandlers:
|
||||
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()
|
||||
@@ -269,6 +276,7 @@ class VoiceHandlers:
|
||||
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')
|
||||
@@ -279,7 +287,7 @@ class VoiceHandlers:
|
||||
if message_with_date:
|
||||
await message.answer(text=message_with_date, parse_mode="html")
|
||||
except Exception as e:
|
||||
logger.error(f'Не удалось получить дату последнего сообщения - {e}')
|
||||
logger.error(f'Не удалось получить дату последнего сообщения для пользователя {message.from_user.id}: {e}')
|
||||
|
||||
await state.set_state(STATE_STANDUP_WRITE)
|
||||
|
||||
@@ -309,6 +317,7 @@ class VoiceHandlers:
|
||||
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)
|
||||
@@ -318,6 +327,7 @@ class VoiceHandlers:
|
||||
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)
|
||||
@@ -326,22 +336,27 @@ class VoiceHandlers:
|
||||
|
||||
@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:
|
||||
@@ -349,7 +364,7 @@ class VoiceHandlers:
|
||||
if message_with_date:
|
||||
await message.answer(text=message_with_date, parse_mode="html")
|
||||
except Exception as e:
|
||||
logger.error(f'Не удалось получить последнюю дату {e}')
|
||||
logger.error(f'Не удалось получить последнюю дату для пользователя {message.from_user.id}: {e}')
|
||||
return
|
||||
|
||||
audio_for_user, date_added, user_emoji = audio_data
|
||||
@@ -359,7 +374,13 @@ class VoiceHandlers:
|
||||
|
||||
# Проверяем существование файла
|
||||
if not path.exists():
|
||||
logger.error(f"Файл не найден: {path}")
|
||||
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
|
||||
@@ -368,7 +389,7 @@ class VoiceHandlers:
|
||||
|
||||
# Проверяем размер файла
|
||||
if path.stat().st_size == 0:
|
||||
logger.error(f"Файл пустой: {path}")
|
||||
logger.error(f"Файл пустой: {path} для пользователя {message.from_user.id}")
|
||||
await message.answer(
|
||||
text="Файл аудио поврежден. Обратитесь к администратору.",
|
||||
reply_markup=markup
|
||||
@@ -383,13 +404,20 @@ class VoiceHandlers:
|
||||
else:
|
||||
caption = f'Дата записи: {date_added}'
|
||||
|
||||
logger.info(f"Подготовлено голосовое сообщение для пользователя {message.from_user.id}: {caption}")
|
||||
|
||||
try:
|
||||
await message.bot.send_voice(
|
||||
chat_id=message.chat.id,
|
||||
voice=voice,
|
||||
caption=caption,
|
||||
reply_markup=markup
|
||||
)
|
||||
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)
|
||||
@@ -404,7 +432,7 @@ class VoiceHandlers:
|
||||
except Exception as voice_error:
|
||||
if "VOICE_MESSAGES_FORBIDDEN" in str(voice_error):
|
||||
# Если голосовые сообщения запрещены, отправляем информативное сообщение
|
||||
logger.info(f"Пользователь {message.from_user.id} запретил получение голосовых сообщений")
|
||||
logger.warning(f"Пользователь {message.from_user.id} запретил получение голосовых сообщений")
|
||||
|
||||
privacy_message = "🔇 К сожалению, у тебя закрыт доступ к получению голосовых сообщений.\n\nДля продолжения взаимодействия с ботом необходимо дать возможность мне присылать войсы в настройках приватности Telegram.\n\n💡 Как это сделать:\n1. Открой настройки Telegram\n2. Перейди в 'Конфиденциальность и безопасность'\n3. Выбери 'Голосовые сообщения'\n4. Разреши получение от 'Всех' или добавь меня в исключения"
|
||||
|
||||
@@ -412,10 +440,11 @@ class VoiceHandlers:
|
||||
return # Выходим без записи о прослушивании
|
||||
|
||||
else:
|
||||
logger.error(f"Ошибка при отправке голосового сообщения пользователю {message.from_user.id}: {voice_error}")
|
||||
raise voice_error
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при прослушивании аудио: {e}")
|
||||
logger.error(f"Ошибка при прослушивании аудио для пользователя {message.from_user.id}: {e}")
|
||||
await message.answer(
|
||||
text="Произошла ошибка при получении аудио. Попробуйте позже.",
|
||||
reply_markup=markup
|
||||
|
||||
Reference in New Issue
Block a user