Enhance admin handlers with improved logging and error handling
- Added detailed logging for user ban processing in `process_ban_target` and `process_ban_reason` functions, including user data and error messages. - Improved error handling for user input validation and database interactions. - Updated `return_to_admin_menu` function to log user return actions. - Enhanced media group handling in `PostPublishService` with better error logging and author ID retrieval. - Added new button options in voice handlers and updated keyboard layouts for improved user interaction. - Refactored album middleware to better handle media group messages and added documentation for clarity.
This commit is contained in:
@@ -145,32 +145,42 @@ async def process_ban_target(
|
||||
bot_db: MagicData("bot_db")
|
||||
):
|
||||
"""Обработка введенного username/ID для блокировки"""
|
||||
logger.info(f"process_ban_target: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}")
|
||||
|
||||
try:
|
||||
user_data = await state.get_data()
|
||||
ban_type = user_data.get('ban_type')
|
||||
admin_service = AdminService(bot_db)
|
||||
|
||||
logger.info(f"process_ban_target: ban_type={ban_type}, user_data={user_data}")
|
||||
|
||||
# Определяем пользователя
|
||||
if ban_type == "username":
|
||||
logger.info(f"process_ban_target: Поиск пользователя по username: {message.text}")
|
||||
user = await admin_service.get_user_by_username(message.text)
|
||||
if not user:
|
||||
logger.warning(f"process_ban_target: Пользователь с username '{message.text}' не найден")
|
||||
await message.answer(f"Пользователь с username '{escape_html(message.text)}' не найден.")
|
||||
await return_to_admin_menu(message, state)
|
||||
return
|
||||
else: # ban_type == "id"
|
||||
try:
|
||||
logger.info(f"process_ban_target: Валидация и поиск пользователя по ID: {message.text}")
|
||||
user_id = await admin_service.validate_user_input(message.text)
|
||||
user = await admin_service.get_user_by_id(user_id)
|
||||
if not user:
|
||||
logger.warning(f"process_ban_target: Пользователь с ID {user_id} не найден в базе данных")
|
||||
await message.answer(f"Пользователь с ID {user_id} не найден в базе данных.")
|
||||
await return_to_admin_menu(message, state)
|
||||
return
|
||||
except InvalidInputError as e:
|
||||
logger.error(f"process_ban_target: Ошибка валидации ID: {e}")
|
||||
await message.answer(str(e))
|
||||
await return_to_admin_menu(message, state)
|
||||
return
|
||||
|
||||
logger.info(f"process_ban_target: Найден пользователь: {user.user_id}, {user.username}, {user.full_name}")
|
||||
|
||||
# Сохраняем данные пользователя
|
||||
await state.update_data(
|
||||
target_user_id=user.user_id,
|
||||
@@ -181,13 +191,17 @@ async def process_ban_target(
|
||||
# Показываем информацию о пользователе и запрашиваем причину
|
||||
user_info = format_user_info(user.user_id, user.username, user.full_name)
|
||||
markup = create_keyboard_for_ban_reason()
|
||||
logger.info(f"process_ban_target: Отправка сообщения с причиной бана, user_info: {user_info}")
|
||||
|
||||
await message.answer(
|
||||
text=f"{user_info}\n\nВыбери причину бана из списка или напиши ее в чат",
|
||||
reply_markup=markup
|
||||
)
|
||||
await state.set_state('AWAIT_BAN_DETAILS')
|
||||
logger.info("process_ban_target: Состояние изменено на AWAIT_BAN_DETAILS")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"process_ban_target: Неожиданная ошибка: {e}", exc_info=True)
|
||||
await handle_admin_error(message, e, state, "process_ban_target")
|
||||
|
||||
|
||||
@@ -201,16 +215,33 @@ async def process_ban_reason(
|
||||
**kwargs
|
||||
):
|
||||
"""Обработка причины блокировки"""
|
||||
logger.info(f"process_ban_reason: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}")
|
||||
|
||||
try:
|
||||
# Проверяем текущее состояние
|
||||
current_state = await state.get_state()
|
||||
logger.info(f"process_ban_reason: Текущее состояние: {current_state}")
|
||||
|
||||
# Проверяем данные состояния
|
||||
state_data = await state.get_data()
|
||||
logger.info(f"process_ban_reason: Данные состояния: {state_data}")
|
||||
|
||||
logger.info(f"process_ban_reason: Обновление данных состояния с причиной: {message.text}")
|
||||
await state.update_data(ban_reason=message.text)
|
||||
|
||||
markup = create_keyboard_for_ban_days()
|
||||
safe_reason = escape_html(message.text)
|
||||
logger.info(f"process_ban_reason: Отправка сообщения с выбором срока бана, причина: {safe_reason}")
|
||||
|
||||
await message.answer(
|
||||
f"Выбрана причина: {safe_reason}. Выбери срок бана в днях или напиши его в чат",
|
||||
reply_markup=markup
|
||||
)
|
||||
await state.set_state('AWAIT_BAN_DURATION')
|
||||
logger.info("process_ban_reason: Состояние изменено на AWAIT_BAN_DURATION")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"process_ban_reason: Неожиданная ошибка: {e}", exc_info=True)
|
||||
await handle_admin_error(message, e, state, "process_ban_reason")
|
||||
|
||||
|
||||
|
||||
@@ -16,14 +16,18 @@ def escape_html(text: str) -> str:
|
||||
async def return_to_admin_menu(message: types.Message, state: FSMContext,
|
||||
additional_message: Optional[str] = None) -> None:
|
||||
"""Универсальная функция для возврата в админ-меню"""
|
||||
logger.info(f"return_to_admin_menu: Возврат в админ-меню для пользователя {message.from_user.id}")
|
||||
|
||||
await state.set_data({})
|
||||
await state.set_state("ADMIN")
|
||||
markup = get_reply_keyboard_admin()
|
||||
|
||||
if additional_message:
|
||||
logger.info(f"return_to_admin_menu: Отправка дополнительного сообщения: {additional_message}")
|
||||
await message.answer(additional_message)
|
||||
|
||||
await message.answer('Вернулись в меню', reply_markup=markup)
|
||||
logger.info(f"return_to_admin_menu: Пользователь {message.from_user.id} успешно возвращен в админ-меню")
|
||||
|
||||
|
||||
async def handle_admin_error(message: types.Message, error: Exception,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import html
|
||||
from datetime import datetime, timedelta
|
||||
import html
|
||||
from typing import Dict, Any
|
||||
|
||||
from aiogram import Bot
|
||||
@@ -42,9 +42,14 @@ class PostPublishService:
|
||||
|
||||
async def publish_post(self, call: CallbackQuery) -> None:
|
||||
"""Основной метод публикации поста"""
|
||||
# Проверяем, является ли сообщение частью медиагруппы
|
||||
if call.message.media_group_id:
|
||||
await self._publish_media_group(call)
|
||||
return
|
||||
|
||||
content_type = call.message.content_type
|
||||
|
||||
if content_type == CONTENT_TYPE_TEXT and call.message.text != CONTENT_TYPE_MEDIA_GROUP:
|
||||
if content_type == CONTENT_TYPE_TEXT:
|
||||
await self._publish_text_post(call)
|
||||
elif content_type == CONTENT_TYPE_PHOTO:
|
||||
await self._publish_photo_post(call)
|
||||
@@ -56,8 +61,6 @@ class PostPublishService:
|
||||
await self._publish_audio_post(call)
|
||||
elif content_type == CONTENT_TYPE_VOICE:
|
||||
await self._publish_voice_post(call)
|
||||
elif call.message.text == CONTENT_TYPE_MEDIA_GROUP:
|
||||
await self._publish_media_group(call)
|
||||
else:
|
||||
raise PublishError(f"Неподдерживаемый тип контента: {content_type}")
|
||||
|
||||
@@ -115,51 +118,111 @@ class PostPublishService:
|
||||
|
||||
async def _publish_media_group(self, call: CallbackQuery) -> None:
|
||||
"""Публикация медиагруппы"""
|
||||
post_content = await self.db.get_post_content_from_telegram_by_last_id(call.message.message_id)
|
||||
pre_text = await self.db.get_post_text_from_telegram_by_last_id(call.message.message_id)
|
||||
post_text = html.escape(str(pre_text))
|
||||
author_id = await self._get_author_id_for_media_group(call.message.message_id)
|
||||
|
||||
await send_media_group_to_channel(bot=self._get_bot(call.message), chat_id=self.main_public, post_content=post_content, post_text=post_text)
|
||||
await self._delete_media_group_and_notify_author(call, author_id)
|
||||
logger.info(f"Начинаю публикацию медиагруппы. Helper message ID: {call.message.message_id}")
|
||||
try:
|
||||
# call.message.message_id - это ID helper сообщения
|
||||
helper_message_id = call.message.message_id
|
||||
|
||||
# Получаем контент медиагруппы по helper_message_id
|
||||
logger.debug(f"Получаю контент медиагруппы для helper_message_id: {helper_message_id}")
|
||||
post_content = await self.db.get_post_content_by_helper_id(helper_message_id)
|
||||
if not post_content:
|
||||
logger.error(f"Контент медиагруппы не найден в базе данных для helper_message_id: {helper_message_id}")
|
||||
raise PublishError("Контент медиагруппы не найден в базе данных")
|
||||
|
||||
# Получаем текст поста по helper_message_id
|
||||
logger.debug(f"Получаю текст поста для helper_message_id: {helper_message_id}")
|
||||
pre_text = await self.db.get_post_text_by_helper_id(helper_message_id)
|
||||
post_text = html.escape(str(pre_text)) if pre_text else ""
|
||||
logger.debug(f"Текст поста получен: {'пустой' if not post_text else f'длина: {len(post_text)} символов'}")
|
||||
|
||||
# Получаем ID автора по helper_message_id
|
||||
logger.debug(f"Получаю ID автора для helper_message_id: {helper_message_id}")
|
||||
author_id = await self.db.get_author_id_by_helper_message_id(helper_message_id)
|
||||
if not author_id:
|
||||
logger.error(f"Автор не найден для медиагруппы {helper_message_id}")
|
||||
raise PostNotFoundError(f"Автор не найден для медиагруппы {helper_message_id}")
|
||||
logger.debug(f"ID автора получен: {author_id}")
|
||||
|
||||
# Отправляем медиагруппу в канал
|
||||
logger.info(f"Отправляю медиагруппу в канал {self.main_public}")
|
||||
await send_media_group_to_channel(
|
||||
bot=self._get_bot(call.message),
|
||||
chat_id=self.main_public,
|
||||
post_content=post_content,
|
||||
post_text=post_text
|
||||
)
|
||||
|
||||
logger.debug(f"Удаляю медиагруппу и уведомляю автора {author_id}")
|
||||
await self._delete_media_group_and_notify_author(call, author_id)
|
||||
logger.info(f'Медиагруппа опубликована в канале {self.main_public}.')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при публикации медиагруппы: {e}")
|
||||
raise PublishError(f"Не удалось опубликовать медиагруппу: {str(e)}")
|
||||
|
||||
async def decline_post(self, call: CallbackQuery) -> None:
|
||||
"""Отклонение поста"""
|
||||
logger.info(f"Начинаю отклонение поста. Message ID: {call.message.message_id}, Content type: {call.message.content_type}")
|
||||
# Проверяем, является ли сообщение частью медиагруппы (осознанный костыль, т.к. сообщение к которому прикреплен коллбек без медиагруппы)
|
||||
if call.message.text == CONTENT_TYPE_MEDIA_GROUP:
|
||||
logger.debug("Сообщение является частью медиагруппы, вызываю _decline_media_group")
|
||||
await self._decline_media_group(call)
|
||||
return
|
||||
|
||||
content_type = call.message.content_type
|
||||
|
||||
if (content_type == CONTENT_TYPE_TEXT and call.message.text != CONTENT_TYPE_MEDIA_GROUP) or \
|
||||
content_type in [CONTENT_TYPE_PHOTO, CONTENT_TYPE_AUDIO, CONTENT_TYPE_VOICE, CONTENT_TYPE_VIDEO, CONTENT_TYPE_VIDEO_NOTE]:
|
||||
if content_type in [CONTENT_TYPE_TEXT, CONTENT_TYPE_PHOTO, CONTENT_TYPE_AUDIO,
|
||||
CONTENT_TYPE_VOICE, CONTENT_TYPE_VIDEO, CONTENT_TYPE_VIDEO_NOTE]:
|
||||
logger.debug(f"Отклоняю одиночный пост типа: {content_type}")
|
||||
await self._decline_single_post(call)
|
||||
elif call.message.text == CONTENT_TYPE_MEDIA_GROUP:
|
||||
await self._decline_media_group(call)
|
||||
else:
|
||||
logger.error(f"Неподдерживаемый тип контента для отклонения: {content_type}")
|
||||
raise PublishError(f"Неподдерживаемый тип контента для отклонения: {content_type}")
|
||||
|
||||
async def _decline_single_post(self, call: CallbackQuery) -> None:
|
||||
"""Отклонение одиночного поста"""
|
||||
logger.debug(f"Отклоняю одиночный пост. Message ID: {call.message.message_id}")
|
||||
author_id = await self._get_author_id(call.message.message_id)
|
||||
logger.debug(f"ID автора получен: {author_id}")
|
||||
|
||||
logger.debug(f"Удаляю сообщение из группы {self.group_for_posts}")
|
||||
await self._get_bot(call.message).delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
|
||||
|
||||
try:
|
||||
logger.debug(f"Отправляю уведомление об отклонении автору {author_id}")
|
||||
await send_text_message(author_id, call.message, MESSAGE_POST_DECLINED)
|
||||
except Exception as e:
|
||||
if str(e) == ERROR_BOT_BLOCKED:
|
||||
logger.warning(f"Пользователь {author_id} заблокировал бота")
|
||||
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||
logger.error(f"Ошибка при отправке уведомления автору {author_id}: {e}")
|
||||
raise
|
||||
logger.info(f'Сообщение отклонено админом {call.from_user.full_name} (ID: {call.from_user.id}).')
|
||||
|
||||
async def _decline_media_group(self, call: CallbackQuery) -> None:
|
||||
"""Отклонение медиагруппы"""
|
||||
logger.debug(f"Отклоняю медиагруппу. Helper message ID: {call.message.message_id}")
|
||||
|
||||
post_ids = await self.db.get_post_ids_from_telegram_by_last_id(call.message.message_id)
|
||||
message_ids = [row[0] for row in post_ids]
|
||||
message_ids = post_ids.copy()
|
||||
message_ids.append(call.message.message_id)
|
||||
logger.debug(f"Получены ID сообщений для удаления: {message_ids}")
|
||||
|
||||
author_id = await self._get_author_id_for_media_group(call.message.message_id)
|
||||
logger.debug(f"ID автора медиагруппы получен: {author_id}")
|
||||
|
||||
logger.debug(f"Удаляю {len(message_ids)} сообщений из группы {self.group_for_posts}")
|
||||
await self._get_bot(call.message).delete_messages(chat_id=self.group_for_posts, message_ids=message_ids)
|
||||
|
||||
try:
|
||||
logger.debug(f"Отправляю уведомление об отклонении автору медиагруппы {author_id}")
|
||||
await send_text_message(author_id, call.message, MESSAGE_POST_DECLINED)
|
||||
except Exception as e:
|
||||
if str(e) == ERROR_BOT_BLOCKED:
|
||||
logger.warning(f"Пользователь {author_id} заблокировал бота")
|
||||
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||
logger.error(f"Ошибка при отправке уведомления автору медиагруппы {author_id}: {e}")
|
||||
raise
|
||||
|
||||
async def _get_author_id(self, message_id: int) -> int:
|
||||
@@ -171,7 +234,27 @@ class PostPublishService:
|
||||
|
||||
async def _get_author_id_for_media_group(self, message_id: int) -> int:
|
||||
"""Получение ID автора для медиагруппы"""
|
||||
# Сначала пытаемся найти автора по helper_message_id
|
||||
author_id = await self.db.get_author_id_by_helper_message_id(message_id)
|
||||
if author_id:
|
||||
return author_id
|
||||
|
||||
# Если не найден, ищем по основному message_id медиагруппы
|
||||
# Для этого нужно найти связанные сообщения медиагруппы
|
||||
try:
|
||||
# Получаем все ID сообщений медиагруппы
|
||||
post_ids = await self.db.get_post_ids_from_telegram_by_last_id(message_id)
|
||||
if post_ids:
|
||||
# Берем первый ID (основное сообщение медиагруппы)
|
||||
main_message_id = post_ids[0]
|
||||
author_id = await self.db.get_author_id_by_message_id(main_message_id)
|
||||
if author_id:
|
||||
return author_id
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось найти автора через связанные сообщения: {e}")
|
||||
|
||||
# Если все способы не сработали, ищем напрямую
|
||||
author_id = await self.db.get_author_id_by_message_id(message_id)
|
||||
if not author_id:
|
||||
raise PostNotFoundError(f"Автор не найден для медиагруппы {message_id}")
|
||||
return author_id
|
||||
@@ -190,9 +273,10 @@ class PostPublishService:
|
||||
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)
|
||||
message_ids = [row[0] for row in post_ids]
|
||||
message_ids.append(call.message.message_id)
|
||||
await self._get_bot(call.message).delete_messages(chat_id=self.group_for_posts, message_ids=message_ids)
|
||||
|
||||
#message_ids = post_ids.copy()
|
||||
post_ids.append(call.message.message_id)
|
||||
await self._get_bot(call.message).delete_messages(chat_id=self.group_for_posts, message_ids=post_ids)
|
||||
try:
|
||||
await send_text_message(author_id, call.message, MESSAGE_POST_PUBLISHED)
|
||||
except Exception as e:
|
||||
|
||||
@@ -272,6 +272,16 @@ class PostService:
|
||||
if album and album[0].caption:
|
||||
post_caption = get_text_message(album[0].caption.lower(), first_name, message.from_user.username)
|
||||
|
||||
# Создаем основной пост для медиагруппы
|
||||
main_post = TelegramPost(
|
||||
message_id=message.message_id, # ID основного сообщения медиагруппы
|
||||
text=post_caption,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await self.db.add_post(main_post)
|
||||
|
||||
# Отправляем медиагруппу в группу для модерации
|
||||
media_group = await prepare_media_group_from_middlewares(album, post_caption)
|
||||
media_group_message_id = await send_media_group_message_to_private_chat(
|
||||
self.settings.group_for_posts, message, media_group, self.db
|
||||
@@ -279,11 +289,24 @@ class PostService:
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
# Создаем helper сообщение с кнопками
|
||||
markup = get_reply_keyboard_for_post()
|
||||
help_message_id = await send_text_message(self.settings.group_for_posts, message, "^", markup)
|
||||
help_message_id = await send_text_message(self.settings.group_for_posts, message, "ВРУЧНУЮ ВЫКЛАДЫВАТЬ, ПОСЛЕ ВЫКЛАДКИ УДАЛИТЬ ОБА ПОСТА")
|
||||
|
||||
# Создаем helper пост и связываем его с основным
|
||||
helper_post = TelegramPost(
|
||||
message_id=help_message_id, # ID helper сообщения
|
||||
text="^", # Специальный маркер для медиагруппы
|
||||
author_id=message.from_user.id,
|
||||
helper_text_message_id=main_post.message_id, # Ссылка на основной пост
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await self.db.add_post(helper_post)
|
||||
|
||||
# Обновляем основной пост, чтобы он ссылался на helper
|
||||
await self.db.update_helper_message(
|
||||
message_id=media_group_message_id, helper_message_id=help_message_id
|
||||
message_id=main_post.message_id,
|
||||
helper_message_id=help_message_id
|
||||
)
|
||||
|
||||
@track_time("process_post", "post_service")
|
||||
@@ -291,7 +314,7 @@ class PostService:
|
||||
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)
|
||||
|
||||
# TODO: Бесит меня этот функционал
|
||||
if message.media_group_id is not None:
|
||||
safe_username = html.escape(message.from_user.username) if message.from_user.username else "Без никнейма"
|
||||
await send_text_message(
|
||||
|
||||
@@ -30,7 +30,10 @@ BTN_LISTEN = "🎧Послушать"
|
||||
# Button to command mapping for metrics
|
||||
BUTTON_COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||
"🎤Высказаться": "voice_speak",
|
||||
"🎧Послушать": "voice_listen"
|
||||
"🎧Послушать": "voice_listen",
|
||||
"Отменить": "voice_cancel",
|
||||
"🔄Сбросить прослушивания": "voice_refresh_listen",
|
||||
"😊Узнать эмодзи": "voice_emoji"
|
||||
}
|
||||
|
||||
# Callback data
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import random
|
||||
import asyncio
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple
|
||||
@@ -255,22 +256,40 @@ class AudioFileService:
|
||||
async def download_and_save_audio(self, bot, message, file_name: str) -> None:
|
||||
"""Скачать и сохранить аудио файл"""
|
||||
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)
|
||||
logger.info(f"Файл скачан, размер: {len(downloaded_file.read()) if downloaded_file else 'None'} bytes")
|
||||
|
||||
# Сбрасываем позицию в файле
|
||||
downloaded_file.seek(0)
|
||||
|
||||
# Создаем директорию если она не существует
|
||||
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}")
|
||||
|
||||
# Сохраняем файл
|
||||
with open(f'{VOICE_USERS_DIR}/{file_name}.ogg', 'wb') as new_file:
|
||||
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}")
|
||||
|
||||
@@ -18,12 +18,14 @@ 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, validate_voice_message, get_user_emoji_safe
|
||||
from helper_bot.keyboards.keyboards import get_main_keyboard, get_reply_keyboard_for_voice
|
||||
from helper_bot.keyboards import get_reply_keyboard
|
||||
from helper_bot.handlers.private.constants import FSM_STATES
|
||||
from helper_bot.handlers.private.constants import BUTTON_TEXTS
|
||||
|
||||
|
||||
class VoiceHandlers:
|
||||
def __init__(self, db, settings):
|
||||
self.db = db
|
||||
self.db = db.get_db() if hasattr(db, 'get_db') else db
|
||||
self.settings = settings
|
||||
self.router = Router()
|
||||
self._setup_handlers()
|
||||
@@ -93,6 +95,25 @@ class VoiceHandlers:
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == BTN_LISTEN
|
||||
)
|
||||
|
||||
# Новые обработчики кнопок
|
||||
self.router.message.register(
|
||||
self.cancel_handler,
|
||||
ChatTypeFilter(chat_type=["private"]),
|
||||
F.text == "Отменить"
|
||||
)
|
||||
|
||||
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 == "😊Узнать эмодзи"
|
||||
)
|
||||
|
||||
async def voice_bot_button_handler(self, message: types.Message, state: FSMContext, bot_db: MagicData("bot_db"), settings: MagicData("settings")):
|
||||
"""Обработчик кнопки 'Голосовой бот' из основной клавиатуры"""
|
||||
@@ -180,6 +201,19 @@ class VoiceHandlers:
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отметке получения приветствия: {e}")
|
||||
|
||||
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"])
|
||||
|
||||
async def refresh_listen_function(
|
||||
self,
|
||||
@@ -334,7 +368,7 @@ class VoiceHandlers:
|
||||
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) - 1
|
||||
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
|
||||
|
||||
@@ -25,13 +25,13 @@ def get_reply_keyboard_for_post():
|
||||
|
||||
@track_time("get_reply_keyboard", "keyboard_service")
|
||||
@track_errors("keyboard_service", "get_reply_keyboard")
|
||||
async def get_reply_keyboard(BotDB, user_id):
|
||||
async def get_reply_keyboard(db, user_id):
|
||||
builder = ReplyKeyboardBuilder()
|
||||
builder.row(types.KeyboardButton(text="📢Предложить свой пост"))
|
||||
builder.row(types.KeyboardButton(text="📩Связаться с админами"))
|
||||
builder.row(types.KeyboardButton(text=" 🎤Голосовой бот"))
|
||||
builder.row(types.KeyboardButton(text="👋🏼Сказать пока!"))
|
||||
if not await BotDB.get_stickers_info(user_id):
|
||||
if not await db.get_stickers_info(user_id):
|
||||
builder.row(types.KeyboardButton(text="🤪Хочу стикеры"))
|
||||
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
|
||||
return markup
|
||||
@@ -175,8 +175,18 @@ def create_keyboard_for_approve_ban():
|
||||
|
||||
def get_main_keyboard():
|
||||
builder = ReplyKeyboardBuilder()
|
||||
builder.add(types.KeyboardButton(text="🎤Высказаться"))
|
||||
builder.add(types.KeyboardButton(text="🎧Послушать"))
|
||||
# Первая строка: Высказаться и послушать
|
||||
builder.row(
|
||||
types.KeyboardButton(text="🎤Высказаться"),
|
||||
types.KeyboardButton(text="🎧Послушать")
|
||||
)
|
||||
# Вторая строка: сбросить прослушивания и узнать эмодзи
|
||||
builder.row(
|
||||
types.KeyboardButton(text="🔄Сбросить прослушивания"),
|
||||
types.KeyboardButton(text="😊Узнать эмодзи")
|
||||
)
|
||||
# Третья строка: Вернуться в меню
|
||||
builder.row(types.KeyboardButton(text="Отменить"))
|
||||
markup = builder.as_markup(resize_keyboard=True)
|
||||
return markup
|
||||
|
||||
|
||||
@@ -1,61 +1,82 @@
|
||||
import asyncio
|
||||
from typing import Any, Dict, Union
|
||||
from typing import Any, Dict, Union, List
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import Message
|
||||
|
||||
|
||||
class AlbumMiddleware(BaseMiddleware):
|
||||
def __init__(self, latency: Union[int, float] = 0.01): # Уменьшено с 0.1 до 0.01
|
||||
# Initialize latency and album_data dictionary
|
||||
"""
|
||||
Middleware для обработки медиа групп в Telegram.
|
||||
Собирает все сообщения одной медиа группы и передает их как album в data.
|
||||
"""
|
||||
|
||||
def __init__(self, latency: Union[int, float] = 0.01):
|
||||
"""
|
||||
Инициализация middleware.
|
||||
|
||||
Args:
|
||||
latency: Задержка в секундах для сбора всех сообщений медиа группы
|
||||
"""
|
||||
super().__init__()
|
||||
self.latency = latency
|
||||
self.album_data = {}
|
||||
self.album_data: Dict[str, Dict[str, List[Message]]] = {}
|
||||
|
||||
#
|
||||
def collect_album_messages(self, event: Message):
|
||||
def collect_album_messages(self, event: Message) -> int:
|
||||
"""
|
||||
Collect messages of the same media group.
|
||||
Собирает сообщения одной медиа группы.
|
||||
|
||||
Args:
|
||||
event: Сообщение для обработки
|
||||
|
||||
Returns:
|
||||
Количество сообщений в текущей медиа группе
|
||||
"""
|
||||
# # Check if media_group_id exists in album_data
|
||||
if not event.media_group_id:
|
||||
return 0
|
||||
|
||||
if event.media_group_id not in self.album_data:
|
||||
# # Create a new entry for the media group
|
||||
self.album_data[event.media_group_id] = {"messages": []}
|
||||
#
|
||||
# # Append the new message to the media group
|
||||
|
||||
self.album_data[event.media_group_id]["messages"].append(event)
|
||||
#
|
||||
# # Return the total number of messages in the current media group
|
||||
return len(self.album_data[event.media_group_id]["messages"])
|
||||
|
||||
#
|
||||
async def __call__(self, handler, event: Message, data: Dict[str, Any]) -> Any:
|
||||
"""
|
||||
Main middleware logic.
|
||||
Основная логика middleware.
|
||||
|
||||
Args:
|
||||
handler: Обработчик события
|
||||
event: Событие (сообщение)
|
||||
data: Данные для передачи в обработчик
|
||||
|
||||
Returns:
|
||||
Результат выполнения обработчика
|
||||
"""
|
||||
# # If the event has no media_group_id, pass it to the handler immediately
|
||||
# Если у события нет media_group_id, передаем его обработчику сразу
|
||||
if not event.media_group_id:
|
||||
return await handler(event, data)
|
||||
#
|
||||
# # Collect messages of the same media group
|
||||
|
||||
# Собираем сообщения одной медиа группы
|
||||
total_before = self.collect_album_messages(event)
|
||||
#
|
||||
# # Wait for a specified latency period
|
||||
|
||||
# Ждем указанный период для сбора всех сообщений
|
||||
await asyncio.sleep(self.latency)
|
||||
#
|
||||
# # Check the total number of messages after the latency
|
||||
|
||||
# Проверяем количество сообщений после задержки
|
||||
total_after = len(self.album_data[event.media_group_id]["messages"])
|
||||
#
|
||||
# # If new messages were added during the latency, exit
|
||||
|
||||
# Если за время задержки добавились новые сообщения, выходим
|
||||
if total_before != total_after:
|
||||
return
|
||||
#
|
||||
# # Sort the album messages by message_id and add to data
|
||||
|
||||
# Сортируем сообщения по message_id и добавляем в data
|
||||
album_messages = self.album_data[event.media_group_id]["messages"]
|
||||
album_messages.sort(key=lambda x: x.message_id)
|
||||
data["album"] = album_messages
|
||||
#
|
||||
# # Remove the media group from tracking to free up memory
|
||||
|
||||
# Удаляем медиа группу из отслеживания для освобождения памяти
|
||||
del self.album_data[event.media_group_id]
|
||||
# # Call the original event handler
|
||||
|
||||
# Вызываем оригинальный обработчик события
|
||||
return await handler(event, data)
|
||||
#
|
||||
|
||||
@@ -13,7 +13,7 @@ except ImportError:
|
||||
_emoji_lib_available = False
|
||||
|
||||
from aiogram import types
|
||||
from aiogram.types import InputMediaPhoto, FSInputFile, InputMediaVideo, InputMediaAudio
|
||||
from aiogram.types import InputMediaPhoto, FSInputFile, InputMediaVideo, InputMediaAudio, InputMediaDocument
|
||||
|
||||
from helper_bot.utils.base_dependency_factory import BaseDependencyFactory, get_global_instance
|
||||
from logs.custom_logger import logger
|
||||
@@ -145,14 +145,14 @@ async def download_file(message: types.Message, file_id: str):
|
||||
|
||||
async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
|
||||
"""
|
||||
Создает MediaGroup.
|
||||
Создает MediaGroup согласно best practices aiogram 3.x.
|
||||
|
||||
Args:
|
||||
album: Album объект из Telegram API.
|
||||
post_caption: Текст подписи к первому фото.
|
||||
album: Album объект из Telegram API (список сообщений).
|
||||
post_caption: Текст подписи к первому медиа файлу.
|
||||
|
||||
Returns:
|
||||
Список InputMediaPhoto (MediaGroup).
|
||||
Список InputMedia объектов для MediaGroup.
|
||||
"""
|
||||
# Экранируем post_caption для безопасного использования в HTML
|
||||
safe_post_caption = html.escape(str(post_caption)) if post_caption else ""
|
||||
@@ -162,34 +162,37 @@ async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
|
||||
for i, message in enumerate(album):
|
||||
if message.photo:
|
||||
file_id = message.photo[-1].file_id
|
||||
media_type = 'photo'
|
||||
# Для фото используем InputMediaPhoto
|
||||
if i == 0: # Первое фото получает подпись
|
||||
media_group.append(InputMediaPhoto(media=file_id, caption=safe_post_caption))
|
||||
else:
|
||||
media_group.append(InputMediaPhoto(media=file_id))
|
||||
elif message.video:
|
||||
file_id = message.video.file_id
|
||||
media_type = 'video'
|
||||
# Для видео используем InputMediaVideo
|
||||
if i == 0: # Первое видео получает подпись
|
||||
media_group.append(InputMediaVideo(media=file_id, caption=safe_post_caption))
|
||||
else:
|
||||
media_group.append(InputMediaVideo(media=file_id))
|
||||
elif message.audio:
|
||||
file_id = message.audio.file_id
|
||||
media_type = 'audio'
|
||||
# Для аудио используем InputMediaAudio
|
||||
if i == 0: # Первое аудио получает подпись
|
||||
media_group.append(InputMediaAudio(media=file_id, caption=safe_post_caption))
|
||||
else:
|
||||
media_group.append(InputMediaAudio(media=file_id))
|
||||
elif message.document:
|
||||
file_id = message.document.file_id
|
||||
# Для документов используем InputMediaDocument (если поддерживается)
|
||||
if i == 0: # Первый документ получает подпись
|
||||
media_group.append(InputMediaDocument(media=file_id, caption=safe_post_caption))
|
||||
else:
|
||||
media_group.append(InputMediaDocument(media=file_id))
|
||||
else:
|
||||
# Если нет фото, видео или аудио, пропускаем сообщение
|
||||
# Если нет поддерживаемого медиа, пропускаем сообщение
|
||||
continue
|
||||
|
||||
# Формируем объект MediaGroup с учетом типа медиа
|
||||
if i == len(album) - 1:
|
||||
if media_type == 'photo':
|
||||
media_group.append(InputMediaPhoto(media=file_id, caption=safe_post_caption))
|
||||
elif media_type == 'video':
|
||||
media_group.append(InputMediaVideo(media=file_id, caption=safe_post_caption))
|
||||
elif media_type == 'audio':
|
||||
media_group.append(InputMediaAudio(media=file_id, caption=safe_post_caption))
|
||||
else:
|
||||
if media_type == 'photo':
|
||||
media_group.append(InputMediaPhoto(media=file_id))
|
||||
elif media_type == 'video':
|
||||
media_group.append(InputMediaVideo(media=file_id))
|
||||
elif media_type == 'audio':
|
||||
media_group.append(InputMediaAudio(media=file_id))
|
||||
|
||||
return media_group # Возвращаем MediaGroup
|
||||
return media_group
|
||||
|
||||
|
||||
async def add_in_db_media_mediagroup(sent_message, bot_db):
|
||||
@@ -280,26 +283,43 @@ async def send_media_group_to_channel(bot, chat_id: int, post_content: List, pos
|
||||
post_content: Список кортежей с путями к файлам.
|
||||
post_text: Текст подписи.
|
||||
"""
|
||||
logger.info(f"Начинаю отправку медиа-группы в чат {chat_id}, количество файлов: {len(post_content)}")
|
||||
|
||||
media = []
|
||||
for file_path in post_content:
|
||||
for i, file_path in enumerate(post_content):
|
||||
try:
|
||||
file = FSInputFile(path=file_path[0])
|
||||
type = file_path[1]
|
||||
logger.debug(f"Обрабатываю файл {i+1}/{len(post_content)}: {file_path[0]} (тип: {type})")
|
||||
|
||||
if type == 'video':
|
||||
media.append(types.InputMediaVideo(media=file))
|
||||
if type == 'photo':
|
||||
elif type == 'photo':
|
||||
media.append(types.InputMediaPhoto(media=file))
|
||||
else:
|
||||
logger.warning(f"Неизвестный тип файла: {type} для {file_path[0]}")
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Файл не найден: {file_path[0]}")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обработке файла {file_path[0]}: {e}")
|
||||
return
|
||||
|
||||
logger.info(f"Подготовлено {len(media)} медиа-файлов для отправки")
|
||||
|
||||
# Добавляем подпись к последнему файлу
|
||||
if media:
|
||||
# Экранируем post_text для безопасного использования в HTML
|
||||
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
||||
media[-1].caption = safe_post_text
|
||||
logger.debug(f"Добавлена подпись к последнему файлу: {safe_post_text[:50]}{'...' if len(safe_post_text) > 50 else ''}")
|
||||
|
||||
await bot.send_media_group(chat_id=chat_id, media=media)
|
||||
try:
|
||||
await bot.send_media_group(chat_id=chat_id, media=media)
|
||||
logger.info(f"Медиа-группа успешно отправлена в чат {chat_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при отправке медиа-группы в чат {chat_id}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def send_text_message(chat_id, message: types.Message, post_text: str, markup: types.ReplyKeyboardMarkup = None):
|
||||
@@ -599,7 +619,7 @@ async def update_user_info(source: str, message: types.Message):
|
||||
@db_query_time("check_emoji_for_user", "users", "select")
|
||||
async def check_user_emoji(message: types.Message):
|
||||
user_id = message.from_user.id
|
||||
user_emoji = await BotDB.get_stickers_info(user_id=user_id)
|
||||
user_emoji = await BotDB.get_user_emoji(user_id=user_id)
|
||||
if user_emoji is None or user_emoji in ("Смайл еще не определен", "Эмоджи не определен", ""):
|
||||
user_emoji = await get_random_emoji()
|
||||
await BotDB.update_user_emoji(user_id=user_id, emoji=user_emoji)
|
||||
|
||||
Reference in New Issue
Block a user