Dev 8 #10
1
.gitignore
vendored
1
.gitignore
vendored
@@ -92,3 +92,4 @@ venv.bak/
|
|||||||
|
|
||||||
# Other files
|
# Other files
|
||||||
voice_users/
|
voice_users/
|
||||||
|
files/
|
||||||
@@ -145,32 +145,42 @@ async def process_ban_target(
|
|||||||
bot_db: MagicData("bot_db")
|
bot_db: MagicData("bot_db")
|
||||||
):
|
):
|
||||||
"""Обработка введенного username/ID для блокировки"""
|
"""Обработка введенного username/ID для блокировки"""
|
||||||
|
logger.info(f"process_ban_target: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user_data = await state.get_data()
|
user_data = await state.get_data()
|
||||||
ban_type = user_data.get('ban_type')
|
ban_type = user_data.get('ban_type')
|
||||||
admin_service = AdminService(bot_db)
|
admin_service = AdminService(bot_db)
|
||||||
|
|
||||||
|
logger.info(f"process_ban_target: ban_type={ban_type}, user_data={user_data}")
|
||||||
|
|
||||||
# Определяем пользователя
|
# Определяем пользователя
|
||||||
if ban_type == "username":
|
if ban_type == "username":
|
||||||
|
logger.info(f"process_ban_target: Поиск пользователя по username: {message.text}")
|
||||||
user = await admin_service.get_user_by_username(message.text)
|
user = await admin_service.get_user_by_username(message.text)
|
||||||
if not user:
|
if not user:
|
||||||
|
logger.warning(f"process_ban_target: Пользователь с username '{message.text}' не найден")
|
||||||
await message.answer(f"Пользователь с username '{escape_html(message.text)}' не найден.")
|
await message.answer(f"Пользователь с username '{escape_html(message.text)}' не найден.")
|
||||||
await return_to_admin_menu(message, state)
|
await return_to_admin_menu(message, state)
|
||||||
return
|
return
|
||||||
else: # ban_type == "id"
|
else: # ban_type == "id"
|
||||||
try:
|
try:
|
||||||
|
logger.info(f"process_ban_target: Валидация и поиск пользователя по ID: {message.text}")
|
||||||
user_id = await admin_service.validate_user_input(message.text)
|
user_id = await admin_service.validate_user_input(message.text)
|
||||||
user = await admin_service.get_user_by_id(user_id)
|
user = await admin_service.get_user_by_id(user_id)
|
||||||
if not user:
|
if not user:
|
||||||
|
logger.warning(f"process_ban_target: Пользователь с ID {user_id} не найден в базе данных")
|
||||||
await message.answer(f"Пользователь с ID {user_id} не найден в базе данных.")
|
await message.answer(f"Пользователь с ID {user_id} не найден в базе данных.")
|
||||||
await return_to_admin_menu(message, state)
|
await return_to_admin_menu(message, state)
|
||||||
return
|
return
|
||||||
except InvalidInputError as e:
|
except InvalidInputError as e:
|
||||||
|
logger.error(f"process_ban_target: Ошибка валидации ID: {e}")
|
||||||
await message.answer(str(e))
|
await message.answer(str(e))
|
||||||
await return_to_admin_menu(message, state)
|
await return_to_admin_menu(message, state)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
logger.info(f"process_ban_target: Найден пользователь: {user.user_id}, {user.username}, {user.full_name}")
|
||||||
|
|
||||||
# Сохраняем данные пользователя
|
# Сохраняем данные пользователя
|
||||||
await state.update_data(
|
await state.update_data(
|
||||||
target_user_id=user.user_id,
|
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)
|
user_info = format_user_info(user.user_id, user.username, user.full_name)
|
||||||
markup = create_keyboard_for_ban_reason()
|
markup = create_keyboard_for_ban_reason()
|
||||||
|
logger.info(f"process_ban_target: Отправка сообщения с причиной бана, user_info: {user_info}")
|
||||||
|
|
||||||
await message.answer(
|
await message.answer(
|
||||||
text=f"{user_info}\n\nВыбери причину бана из списка или напиши ее в чат",
|
text=f"{user_info}\n\nВыбери причину бана из списка или напиши ее в чат",
|
||||||
reply_markup=markup
|
reply_markup=markup
|
||||||
)
|
)
|
||||||
await state.set_state('AWAIT_BAN_DETAILS')
|
await state.set_state('AWAIT_BAN_DETAILS')
|
||||||
|
logger.info("process_ban_target: Состояние изменено на AWAIT_BAN_DETAILS")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"process_ban_target: Неожиданная ошибка: {e}", exc_info=True)
|
||||||
await handle_admin_error(message, e, state, "process_ban_target")
|
await handle_admin_error(message, e, state, "process_ban_target")
|
||||||
|
|
||||||
|
|
||||||
@@ -201,16 +215,33 @@ async def process_ban_reason(
|
|||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
"""Обработка причины блокировки"""
|
"""Обработка причины блокировки"""
|
||||||
|
logger.info(f"process_ban_reason: === НАЧАЛО ОБРАБОТКИ === Получено сообщение от {message.from_user.id}: {message.text}")
|
||||||
|
|
||||||
try:
|
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)
|
await state.update_data(ban_reason=message.text)
|
||||||
|
|
||||||
markup = create_keyboard_for_ban_days()
|
markup = create_keyboard_for_ban_days()
|
||||||
safe_reason = escape_html(message.text)
|
safe_reason = escape_html(message.text)
|
||||||
|
logger.info(f"process_ban_reason: Отправка сообщения с выбором срока бана, причина: {safe_reason}")
|
||||||
|
|
||||||
await message.answer(
|
await message.answer(
|
||||||
f"Выбрана причина: {safe_reason}. Выбери срок бана в днях или напиши его в чат",
|
f"Выбрана причина: {safe_reason}. Выбери срок бана в днях или напиши его в чат",
|
||||||
reply_markup=markup
|
reply_markup=markup
|
||||||
)
|
)
|
||||||
await state.set_state('AWAIT_BAN_DURATION')
|
await state.set_state('AWAIT_BAN_DURATION')
|
||||||
|
logger.info("process_ban_reason: Состояние изменено на AWAIT_BAN_DURATION")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"process_ban_reason: Неожиданная ошибка: {e}", exc_info=True)
|
||||||
await handle_admin_error(message, e, state, "process_ban_reason")
|
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,
|
async def return_to_admin_menu(message: types.Message, state: FSMContext,
|
||||||
additional_message: Optional[str] = None) -> None:
|
additional_message: Optional[str] = None) -> None:
|
||||||
"""Универсальная функция для возврата в админ-меню"""
|
"""Универсальная функция для возврата в админ-меню"""
|
||||||
|
logger.info(f"return_to_admin_menu: Возврат в админ-меню для пользователя {message.from_user.id}")
|
||||||
|
|
||||||
await state.set_data({})
|
await state.set_data({})
|
||||||
await state.set_state("ADMIN")
|
await state.set_state("ADMIN")
|
||||||
markup = get_reply_keyboard_admin()
|
markup = get_reply_keyboard_admin()
|
||||||
|
|
||||||
if additional_message:
|
if additional_message:
|
||||||
|
logger.info(f"return_to_admin_menu: Отправка дополнительного сообщения: {additional_message}")
|
||||||
await message.answer(additional_message)
|
await message.answer(additional_message)
|
||||||
|
|
||||||
await message.answer('Вернулись в меню', reply_markup=markup)
|
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,
|
async def handle_admin_error(message: types.Message, error: Exception,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import html
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import html
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
@@ -42,9 +42,14 @@ class PostPublishService:
|
|||||||
|
|
||||||
async def publish_post(self, call: CallbackQuery) -> None:
|
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
|
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)
|
await self._publish_text_post(call)
|
||||||
elif content_type == CONTENT_TYPE_PHOTO:
|
elif content_type == CONTENT_TYPE_PHOTO:
|
||||||
await self._publish_photo_post(call)
|
await self._publish_photo_post(call)
|
||||||
@@ -56,8 +61,6 @@ class PostPublishService:
|
|||||||
await self._publish_audio_post(call)
|
await self._publish_audio_post(call)
|
||||||
elif content_type == CONTENT_TYPE_VOICE:
|
elif content_type == CONTENT_TYPE_VOICE:
|
||||||
await self._publish_voice_post(call)
|
await self._publish_voice_post(call)
|
||||||
elif call.message.text == CONTENT_TYPE_MEDIA_GROUP:
|
|
||||||
await self._publish_media_group(call)
|
|
||||||
else:
|
else:
|
||||||
raise PublishError(f"Неподдерживаемый тип контента: {content_type}")
|
raise PublishError(f"Неподдерживаемый тип контента: {content_type}")
|
||||||
|
|
||||||
@@ -115,51 +118,111 @@ class PostPublishService:
|
|||||||
|
|
||||||
async def _publish_media_group(self, call: CallbackQuery) -> None:
|
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)
|
logger.info(f"Начинаю публикацию медиагруппы. Helper message ID: {call.message.message_id}")
|
||||||
pre_text = await self.db.get_post_text_from_telegram_by_last_id(call.message.message_id)
|
try:
|
||||||
post_text = html.escape(str(pre_text))
|
# call.message.message_id - это ID helper сообщения
|
||||||
author_id = await self._get_author_id_for_media_group(call.message.message_id)
|
helper_message_id = 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)
|
# Получаем контент медиагруппы по 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)
|
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:
|
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
|
content_type = call.message.content_type
|
||||||
|
|
||||||
if (content_type == CONTENT_TYPE_TEXT and call.message.text != CONTENT_TYPE_MEDIA_GROUP) or \
|
if content_type in [CONTENT_TYPE_TEXT, CONTENT_TYPE_PHOTO, CONTENT_TYPE_AUDIO,
|
||||||
content_type in [CONTENT_TYPE_PHOTO, CONTENT_TYPE_AUDIO, CONTENT_TYPE_VOICE, CONTENT_TYPE_VIDEO, CONTENT_TYPE_VIDEO_NOTE]:
|
CONTENT_TYPE_VOICE, CONTENT_TYPE_VIDEO, CONTENT_TYPE_VIDEO_NOTE]:
|
||||||
|
logger.debug(f"Отклоняю одиночный пост типа: {content_type}")
|
||||||
await self._decline_single_post(call)
|
await self._decline_single_post(call)
|
||||||
elif call.message.text == CONTENT_TYPE_MEDIA_GROUP:
|
|
||||||
await self._decline_media_group(call)
|
|
||||||
else:
|
else:
|
||||||
|
logger.error(f"Неподдерживаемый тип контента для отклонения: {content_type}")
|
||||||
raise PublishError(f"Неподдерживаемый тип контента для отклонения: {content_type}")
|
raise PublishError(f"Неподдерживаемый тип контента для отклонения: {content_type}")
|
||||||
|
|
||||||
async def _decline_single_post(self, call: CallbackQuery) -> None:
|
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)
|
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)
|
await self._get_bot(call.message).delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
logger.debug(f"Отправляю уведомление об отклонении автору {author_id}")
|
||||||
await send_text_message(author_id, call.message, MESSAGE_POST_DECLINED)
|
await send_text_message(author_id, call.message, MESSAGE_POST_DECLINED)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if str(e) == ERROR_BOT_BLOCKED:
|
if str(e) == ERROR_BOT_BLOCKED:
|
||||||
|
logger.warning(f"Пользователь {author_id} заблокировал бота")
|
||||||
raise UserBlockedBotError("Пользователь заблокировал бота")
|
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||||
|
logger.error(f"Ошибка при отправке уведомления автору {author_id}: {e}")
|
||||||
raise
|
raise
|
||||||
logger.info(f'Сообщение отклонено админом {call.from_user.full_name} (ID: {call.from_user.id}).')
|
logger.info(f'Сообщение отклонено админом {call.from_user.full_name} (ID: {call.from_user.id}).')
|
||||||
|
|
||||||
async def _decline_media_group(self, call: CallbackQuery) -> None:
|
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)
|
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)
|
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)
|
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)
|
await self._get_bot(call.message).delete_messages(chat_id=self.group_for_posts, message_ids=message_ids)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
logger.debug(f"Отправляю уведомление об отклонении автору медиагруппы {author_id}")
|
||||||
await send_text_message(author_id, call.message, MESSAGE_POST_DECLINED)
|
await send_text_message(author_id, call.message, MESSAGE_POST_DECLINED)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if str(e) == ERROR_BOT_BLOCKED:
|
if str(e) == ERROR_BOT_BLOCKED:
|
||||||
|
logger.warning(f"Пользователь {author_id} заблокировал бота")
|
||||||
raise UserBlockedBotError("Пользователь заблокировал бота")
|
raise UserBlockedBotError("Пользователь заблокировал бота")
|
||||||
|
logger.error(f"Ошибка при отправке уведомления автору медиагруппы {author_id}: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def _get_author_id(self, message_id: int) -> int:
|
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:
|
async def _get_author_id_for_media_group(self, message_id: int) -> int:
|
||||||
"""Получение ID автора для медиагруппы"""
|
"""Получение ID автора для медиагруппы"""
|
||||||
|
# Сначала пытаемся найти автора по helper_message_id
|
||||||
author_id = await self.db.get_author_id_by_helper_message_id(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:
|
if not author_id:
|
||||||
raise PostNotFoundError(f"Автор не найден для медиагруппы {message_id}")
|
raise PostNotFoundError(f"Автор не найден для медиагруппы {message_id}")
|
||||||
return author_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:
|
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)
|
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)
|
#message_ids = post_ids.copy()
|
||||||
await self._get_bot(call.message).delete_messages(chat_id=self.group_for_posts, message_ids=message_ids)
|
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:
|
try:
|
||||||
await send_text_message(author_id, call.message, MESSAGE_POST_PUBLISHED)
|
await send_text_message(author_id, call.message, MESSAGE_POST_PUBLISHED)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -272,6 +272,16 @@ class PostService:
|
|||||||
if album and album[0].caption:
|
if album and album[0].caption:
|
||||||
post_caption = get_text_message(album[0].caption.lower(), first_name, message.from_user.username)
|
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 = await prepare_media_group_from_middlewares(album, post_caption)
|
||||||
media_group_message_id = await send_media_group_message_to_private_chat(
|
media_group_message_id = await send_media_group_message_to_private_chat(
|
||||||
self.settings.group_for_posts, message, media_group, self.db
|
self.settings.group_for_posts, message, media_group, self.db
|
||||||
@@ -279,11 +289,24 @@ class PostService:
|
|||||||
|
|
||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
|
|
||||||
|
# Создаем helper сообщение с кнопками
|
||||||
markup = get_reply_keyboard_for_post()
|
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(
|
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")
|
@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:
|
async def process_post(self, message: types.Message, album: Union[list, None] = None) -> None:
|
||||||
"""Process post based on content type"""
|
"""Process post based on content type"""
|
||||||
first_name = get_first_name(message)
|
first_name = get_first_name(message)
|
||||||
|
# TODO: Бесит меня этот функционал
|
||||||
if message.media_group_id is not None:
|
if message.media_group_id is not None:
|
||||||
safe_username = html.escape(message.from_user.username) if message.from_user.username else "Без никнейма"
|
safe_username = html.escape(message.from_user.username) if message.from_user.username else "Без никнейма"
|
||||||
await send_text_message(
|
await send_text_message(
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ BTN_LISTEN = "🎧Послушать"
|
|||||||
# Button to command mapping for metrics
|
# Button to command mapping for metrics
|
||||||
BUTTON_COMMAND_MAPPING: Final[Dict[str, str]] = {
|
BUTTON_COMMAND_MAPPING: Final[Dict[str, str]] = {
|
||||||
"🎤Высказаться": "voice_speak",
|
"🎤Высказаться": "voice_speak",
|
||||||
"🎧Послушать": "voice_listen"
|
"🎧Послушать": "voice_listen",
|
||||||
|
"Отменить": "voice_cancel",
|
||||||
|
"🔄Сбросить прослушивания": "voice_refresh_listen",
|
||||||
|
"😊Узнать эмодзи": "voice_emoji"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Callback data
|
# Callback data
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import random
|
import random
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional, Tuple
|
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:
|
async def download_and_save_audio(self, bot, message, file_name: str) -> None:
|
||||||
"""Скачать и сохранить аудио файл"""
|
"""Скачать и сохранить аудио файл"""
|
||||||
try:
|
try:
|
||||||
|
logger.info(f"Начинаем скачивание и сохранение аудио: {file_name}")
|
||||||
|
|
||||||
# Проверяем наличие голосового сообщения
|
# Проверяем наличие голосового сообщения
|
||||||
if not message or not message.voice:
|
if not message or not message.voice:
|
||||||
|
logger.error("Сообщение или голосовое сообщение не найдено")
|
||||||
raise FileOperationError("Сообщение или голосовое сообщение не найдено")
|
raise FileOperationError("Сообщение или голосовое сообщение не найдено")
|
||||||
|
|
||||||
file_id = message.voice.file_id
|
file_id = message.voice.file_id
|
||||||
|
logger.info(f"Получен file_id: {file_id}")
|
||||||
|
|
||||||
file_info = await bot.get_file(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)
|
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
|
import os
|
||||||
os.makedirs(VOICE_USERS_DIR, exist_ok=True)
|
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())
|
new_file.write(downloaded_file.read())
|
||||||
|
|
||||||
|
logger.info(f"Файл успешно сохранен: {file_path}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при скачивании и сохранении аудио: {e}")
|
logger.error(f"Ошибка при скачивании и сохранении аудио: {e}")
|
||||||
|
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||||
raise FileOperationError(f"Не удалось скачать и сохранить аудио: {e}")
|
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.services import VoiceBotService
|
||||||
from helper_bot.handlers.voice.utils import get_last_message_text, validate_voice_message, get_user_emoji_safe
|
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.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
|
from helper_bot.handlers.private.constants import BUTTON_TEXTS
|
||||||
|
|
||||||
|
|
||||||
class VoiceHandlers:
|
class VoiceHandlers:
|
||||||
def __init__(self, db, settings):
|
def __init__(self, db, settings):
|
||||||
self.db = db
|
self.db = db.get_db() if hasattr(db, 'get_db') else db
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.router = Router()
|
self.router = Router()
|
||||||
self._setup_handlers()
|
self._setup_handlers()
|
||||||
@@ -94,6 +96,25 @@ class VoiceHandlers:
|
|||||||
F.text == BTN_LISTEN
|
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")):
|
async def voice_bot_button_handler(self, message: types.Message, state: FSMContext, bot_db: MagicData("bot_db"), settings: MagicData("settings")):
|
||||||
"""Обработчик кнопки 'Голосовой бот' из основной клавиатуры"""
|
"""Обработчик кнопки 'Голосовой бот' из основной клавиатуры"""
|
||||||
try:
|
try:
|
||||||
@@ -180,6 +201,19 @@ class VoiceHandlers:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка при отметке получения приветствия: {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(
|
async def refresh_listen_function(
|
||||||
self,
|
self,
|
||||||
@@ -334,7 +368,7 @@ class VoiceHandlers:
|
|||||||
await voice_service.mark_audio_as_listened(audio_for_user, message.from_user.id)
|
await voice_service.mark_audio_as_listened(audio_for_user, message.from_user.id)
|
||||||
|
|
||||||
# Получаем количество оставшихся аудио только после успешной отправки
|
# Получаем количество оставшихся аудио только после успешной отправки
|
||||||
remaining_count = await voice_service.get_remaining_audio_count(message.from_user.id) - 1
|
remaining_count = await voice_service.get_remaining_audio_count(message.from_user.id)
|
||||||
await message.answer(
|
await message.answer(
|
||||||
text=f'Осталось непрослушанных: <b>{remaining_count}</b>',
|
text=f'Осталось непрослушанных: <b>{remaining_count}</b>',
|
||||||
reply_markup=markup
|
reply_markup=markup
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ def get_reply_keyboard_for_post():
|
|||||||
|
|
||||||
@track_time("get_reply_keyboard", "keyboard_service")
|
@track_time("get_reply_keyboard", "keyboard_service")
|
||||||
@track_errors("keyboard_service", "get_reply_keyboard")
|
@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 = ReplyKeyboardBuilder()
|
||||||
builder.row(types.KeyboardButton(text="📢Предложить свой пост"))
|
builder.row(types.KeyboardButton(text="📢Предложить свой пост"))
|
||||||
builder.row(types.KeyboardButton(text="📩Связаться с админами"))
|
builder.row(types.KeyboardButton(text="📩Связаться с админами"))
|
||||||
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="🤪Хочу стикеры"))
|
builder.row(types.KeyboardButton(text="🤪Хочу стикеры"))
|
||||||
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
|
markup = builder.as_markup(resize_keyboard=True, one_time_keyboard=True)
|
||||||
return markup
|
return markup
|
||||||
@@ -175,8 +175,18 @@ def create_keyboard_for_approve_ban():
|
|||||||
|
|
||||||
def get_main_keyboard():
|
def get_main_keyboard():
|
||||||
builder = ReplyKeyboardBuilder()
|
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)
|
markup = builder.as_markup(resize_keyboard=True)
|
||||||
return markup
|
return markup
|
||||||
|
|
||||||
|
|||||||
@@ -1,61 +1,82 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from typing import Any, Dict, Union
|
from typing import Any, Dict, Union, List
|
||||||
|
|
||||||
from aiogram import BaseMiddleware
|
from aiogram import BaseMiddleware
|
||||||
from aiogram.types import Message
|
from aiogram.types import Message
|
||||||
|
|
||||||
|
|
||||||
class AlbumMiddleware(BaseMiddleware):
|
class AlbumMiddleware(BaseMiddleware):
|
||||||
def __init__(self, latency: Union[int, float] = 0.01): # Уменьшено с 0.1 до 0.01
|
"""
|
||||||
# Initialize latency and album_data dictionary
|
Middleware для обработки медиа групп в Telegram.
|
||||||
self.latency = latency
|
Собирает все сообщения одной медиа группы и передает их как album в data.
|
||||||
self.album_data = {}
|
"""
|
||||||
|
|
||||||
#
|
def __init__(self, latency: Union[int, float] = 0.01):
|
||||||
def collect_album_messages(self, event: Message):
|
|
||||||
"""
|
"""
|
||||||
Collect messages of the same media group.
|
Инициализация middleware.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
latency: Задержка в секундах для сбора всех сообщений медиа группы
|
||||||
"""
|
"""
|
||||||
# # Check if media_group_id exists in album_data
|
super().__init__()
|
||||||
|
self.latency = latency
|
||||||
|
self.album_data: Dict[str, Dict[str, List[Message]]] = {}
|
||||||
|
|
||||||
|
def collect_album_messages(self, event: Message) -> int:
|
||||||
|
"""
|
||||||
|
Собирает сообщения одной медиа группы.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Сообщение для обработки
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Количество сообщений в текущей медиа группе
|
||||||
|
"""
|
||||||
|
if not event.media_group_id:
|
||||||
|
return 0
|
||||||
|
|
||||||
if event.media_group_id not in self.album_data:
|
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": []}
|
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)
|
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"])
|
return len(self.album_data[event.media_group_id]["messages"])
|
||||||
|
|
||||||
#
|
|
||||||
async def __call__(self, handler, event: Message, data: Dict[str, Any]) -> Any:
|
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:
|
if not event.media_group_id:
|
||||||
return await handler(event, data)
|
return await handler(event, data)
|
||||||
#
|
|
||||||
# # Collect messages of the same media group
|
# Собираем сообщения одной медиа группы
|
||||||
total_before = self.collect_album_messages(event)
|
total_before = self.collect_album_messages(event)
|
||||||
#
|
|
||||||
# # Wait for a specified latency period
|
# Ждем указанный период для сбора всех сообщений
|
||||||
await asyncio.sleep(self.latency)
|
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"])
|
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:
|
if total_before != total_after:
|
||||||
return
|
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 = self.album_data[event.media_group_id]["messages"]
|
||||||
album_messages.sort(key=lambda x: x.message_id)
|
album_messages.sort(key=lambda x: x.message_id)
|
||||||
data["album"] = album_messages
|
data["album"] = album_messages
|
||||||
#
|
|
||||||
# # Remove the media group from tracking to free up memory
|
# Удаляем медиа группу из отслеживания для освобождения памяти
|
||||||
del self.album_data[event.media_group_id]
|
del self.album_data[event.media_group_id]
|
||||||
# # Call the original event handler
|
|
||||||
|
# Вызываем оригинальный обработчик события
|
||||||
return await handler(event, data)
|
return await handler(event, data)
|
||||||
#
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ except ImportError:
|
|||||||
_emoji_lib_available = False
|
_emoji_lib_available = False
|
||||||
|
|
||||||
from aiogram import types
|
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 helper_bot.utils.base_dependency_factory import BaseDependencyFactory, get_global_instance
|
||||||
from logs.custom_logger import logger
|
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 = ''):
|
async def prepare_media_group_from_middlewares(album, post_caption: str = ''):
|
||||||
"""
|
"""
|
||||||
Создает MediaGroup.
|
Создает MediaGroup согласно best practices aiogram 3.x.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
album: Album объект из Telegram API.
|
album: Album объект из Telegram API (список сообщений).
|
||||||
post_caption: Текст подписи к первому фото.
|
post_caption: Текст подписи к первому медиа файлу.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Список InputMediaPhoto (MediaGroup).
|
Список InputMedia объектов для MediaGroup.
|
||||||
"""
|
"""
|
||||||
# Экранируем post_caption для безопасного использования в HTML
|
# Экранируем post_caption для безопасного использования в HTML
|
||||||
safe_post_caption = html.escape(str(post_caption)) if post_caption else ""
|
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):
|
for i, message in enumerate(album):
|
||||||
if message.photo:
|
if message.photo:
|
||||||
file_id = message.photo[-1].file_id
|
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:
|
elif message.video:
|
||||||
file_id = message.video.file_id
|
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:
|
elif message.audio:
|
||||||
file_id = message.audio.file_id
|
file_id = message.audio.file_id
|
||||||
media_type = 'audio'
|
# Для аудио используем InputMediaAudio
|
||||||
else:
|
if i == 0: # Первое аудио получает подпись
|
||||||
# Если нет фото, видео или аудио, пропускаем сообщение
|
|
||||||
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))
|
media_group.append(InputMediaAudio(media=file_id, caption=safe_post_caption))
|
||||||
else:
|
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))
|
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
|
||||||
|
|
||||||
return media_group # Возвращаем MediaGroup
|
return media_group
|
||||||
|
|
||||||
|
|
||||||
async def add_in_db_media_mediagroup(sent_message, bot_db):
|
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_content: Список кортежей с путями к файлам.
|
||||||
post_text: Текст подписи.
|
post_text: Текст подписи.
|
||||||
"""
|
"""
|
||||||
|
logger.info(f"Начинаю отправку медиа-группы в чат {chat_id}, количество файлов: {len(post_content)}")
|
||||||
|
|
||||||
media = []
|
media = []
|
||||||
for file_path in post_content:
|
for i, file_path in enumerate(post_content):
|
||||||
try:
|
try:
|
||||||
file = FSInputFile(path=file_path[0])
|
file = FSInputFile(path=file_path[0])
|
||||||
type = file_path[1]
|
type = file_path[1]
|
||||||
|
logger.debug(f"Обрабатываю файл {i+1}/{len(post_content)}: {file_path[0]} (тип: {type})")
|
||||||
|
|
||||||
if type == 'video':
|
if type == 'video':
|
||||||
media.append(types.InputMediaVideo(media=file))
|
media.append(types.InputMediaVideo(media=file))
|
||||||
if type == 'photo':
|
elif type == 'photo':
|
||||||
media.append(types.InputMediaPhoto(media=file))
|
media.append(types.InputMediaPhoto(media=file))
|
||||||
|
else:
|
||||||
|
logger.warning(f"Неизвестный тип файла: {type} для {file_path[0]}")
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.error(f"Файл не найден: {file_path[0]}")
|
logger.error(f"Файл не найден: {file_path[0]}")
|
||||||
return
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при обработке файла {file_path[0]}: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(f"Подготовлено {len(media)} медиа-файлов для отправки")
|
||||||
|
|
||||||
# Добавляем подпись к последнему файлу
|
# Добавляем подпись к последнему файлу
|
||||||
if media:
|
if media:
|
||||||
# Экранируем post_text для безопасного использования в HTML
|
# Экранируем post_text для безопасного использования в HTML
|
||||||
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
||||||
media[-1].caption = safe_post_text
|
media[-1].caption = safe_post_text
|
||||||
|
logger.debug(f"Добавлена подпись к последнему файлу: {safe_post_text[:50]}{'...' if len(safe_post_text) > 50 else ''}")
|
||||||
|
|
||||||
|
try:
|
||||||
await bot.send_media_group(chat_id=chat_id, media=media)
|
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):
|
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")
|
@db_query_time("check_emoji_for_user", "users", "select")
|
||||||
async def check_user_emoji(message: types.Message):
|
async def check_user_emoji(message: types.Message):
|
||||||
user_id = message.from_user.id
|
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 ("Смайл еще не определен", "Эмоджи не определен", ""):
|
if user_emoji is None or user_emoji in ("Смайл еще не определен", "Эмоджи не определен", ""):
|
||||||
user_emoji = await get_random_emoji()
|
user_emoji = await get_random_emoji()
|
||||||
await BotDB.update_user_emoji(user_id=user_id, emoji=user_emoji)
|
await BotDB.update_user_emoji(user_id=user_id, emoji=user_emoji)
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
# Тесты для PostRepository
|
|
||||||
|
|
||||||
Этот документ описывает тесты для `PostRepository` - репозитория для работы с постами из Telegram.
|
|
||||||
|
|
||||||
## Структура тестов
|
|
||||||
|
|
||||||
### 1. `test_post_repository.py` - Unit тесты
|
|
||||||
Содержит модульные тесты с моками для всех методов `PostRepository`:
|
|
||||||
|
|
||||||
- **`test_create_tables`** - тест создания таблиц БД
|
|
||||||
- **`test_add_post_with_date`** - тест добавления поста с датой
|
|
||||||
- **`test_add_post_without_date`** - тест добавления поста без даты (автогенерация)
|
|
||||||
- **`test_add_post_logs_correctly`** - тест логирования при добавлении поста
|
|
||||||
- **`test_update_helper_message`** - тест обновления helper сообщения
|
|
||||||
- **`test_add_post_content_success`** - тест успешного добавления контента
|
|
||||||
- **`test_add_post_content_exception`** - тест обработки исключений
|
|
||||||
- **`test_get_post_content_by_helper_id`** - тест получения контента по helper ID
|
|
||||||
- **`test_get_post_text_by_helper_id_found`** - тест получения текста поста (найден)
|
|
||||||
- **`test_get_post_text_by_helper_id_not_found`** - тест получения текста поста (не найден)
|
|
||||||
- **`test_get_post_ids_by_helper_id`** - тест получения ID сообщений
|
|
||||||
- **`test_get_author_id_by_message_id_found`** - тест получения ID автора по message ID (найден)
|
|
||||||
- **`test_get_author_id_by_message_id_not_found`** - тест получения ID автора по message ID (не найден)
|
|
||||||
- **`test_get_author_id_by_helper_message_id_found`** - тест получения ID автора по helper message ID (найден)
|
|
||||||
- **`test_get_author_id_by_helper_message_id_not_found`** - тест получения ID автора по helper message ID (не найден)
|
|
||||||
- **`test_create_tables_logs_success`** - тест логирования успешного создания таблиц
|
|
||||||
|
|
||||||
### 2. `test_post_repository_integration.py` - Интеграционные тесты
|
|
||||||
Содержит тесты с реальной базой данных SQLite:
|
|
||||||
|
|
||||||
- **`test_create_tables_integration`** - интеграционный тест создания таблиц
|
|
||||||
- **`test_add_post_integration`** - интеграционный тест добавления поста
|
|
||||||
- **`test_add_post_without_date_integration`** - интеграционный тест добавления поста без даты
|
|
||||||
- **`test_update_helper_message_integration`** - интеграционный тест обновления helper сообщения
|
|
||||||
- **`test_add_post_content_integration`** - интеграционный тест добавления контента поста
|
|
||||||
- **`test_add_post_content_with_helper_message_integration`** - интеграционный тест добавления контента с helper сообщением
|
|
||||||
- **`test_get_post_text_by_helper_id_integration`** - интеграционный тест получения текста поста
|
|
||||||
- **`test_get_post_text_by_helper_id_not_found_integration`** - интеграционный тест получения текста несуществующего поста
|
|
||||||
- **`test_get_post_ids_by_helper_id_integration`** - интеграционный тест получения ID сообщений
|
|
||||||
- **`test_get_author_id_by_message_id_integration`** - интеграционный тест получения ID автора по message ID
|
|
||||||
- **`test_get_author_id_by_message_id_not_found_integration`** - интеграционный тест получения ID автора несуществующего поста
|
|
||||||
- **`test_get_author_id_by_helper_message_id_integration`** - интеграционный тест получения ID автора по helper message ID
|
|
||||||
- **`test_get_author_id_by_helper_message_id_not_found_integration`** - интеграционный тест получения ID автора несуществующего helper сообщения
|
|
||||||
- **`test_multiple_posts_integration`** - интеграционный тест работы с несколькими постами
|
|
||||||
- **`test_post_content_relationships_integration`** - интеграционный тест связей между постами и контентом
|
|
||||||
|
|
||||||
### 3. `conftest_post_repository.py` - Общие фикстуры
|
|
||||||
Содержит фикстуры для всех тестов:
|
|
||||||
|
|
||||||
- **`mock_post_repository`** - мок PostRepository для unit тестов
|
|
||||||
- **`sample_telegram_post`** - тестовый объект TelegramPost
|
|
||||||
- **`sample_telegram_post_with_helper`** - тестовый объект TelegramPost с helper сообщением
|
|
||||||
- **`sample_telegram_post_no_date`** - тестовый объект TelegramPost без даты
|
|
||||||
- **`sample_post_content`** - тестовый объект PostContent
|
|
||||||
- **`sample_message_content_link`** - тестовый объект MessageContentLink
|
|
||||||
- **`mock_db_execute_query`** - мок для _execute_query
|
|
||||||
- **`mock_db_execute_query_with_result`** - мок для _execute_query_with_result
|
|
||||||
- **`mock_logger`** - мок для logger
|
|
||||||
- **`temp_db_file`** - временный файл БД для интеграционных тестов
|
|
||||||
- **`real_post_repository`** - реальный PostRepository с временной БД
|
|
||||||
- **`sample_posts_batch`** - набор тестовых постов для batch тестов
|
|
||||||
- **`sample_content_batch`** - набор тестового контента для batch тестов
|
|
||||||
- **`mock_database_connection`** - мок для DatabaseConnection
|
|
||||||
- **`sample_helper_message_ids`** - набор тестовых helper message ID
|
|
||||||
- **`sample_message_ids`** - набор тестовых message ID
|
|
||||||
- **`sample_author_ids`** - набор тестовых author ID
|
|
||||||
- **`mock_sql_queries`** - мок для SQL запросов
|
|
||||||
|
|
||||||
## Запуск тестов
|
|
||||||
|
|
||||||
### Запуск всех тестов для PostRepository:
|
|
||||||
```bash
|
|
||||||
pytest tests/test_post_repository.py -v
|
|
||||||
pytest tests/test_post_repository_integration.py -v
|
|
||||||
```
|
|
||||||
|
|
||||||
### Запуск с покрытием:
|
|
||||||
```bash
|
|
||||||
pytest tests/test_post_repository.py --cov=database.repositories.post_repository --cov-report=html
|
|
||||||
pytest tests/test_post_repository_integration.py --cov=database.repositories.post_repository --cov-report=html
|
|
||||||
```
|
|
||||||
|
|
||||||
### Запуск конкретного теста:
|
|
||||||
```bash
|
|
||||||
pytest tests/test_post_repository.py::TestPostRepository::test_add_post_with_date -v
|
|
||||||
```
|
|
||||||
|
|
||||||
## Требования
|
|
||||||
|
|
||||||
- `pytest` - фреймворк для тестирования
|
|
||||||
- `pytest-asyncio` - поддержка асинхронных тестов
|
|
||||||
- `pytest-cov` - для измерения покрытия кода (опционально)
|
|
||||||
|
|
||||||
## Особенности тестирования
|
|
||||||
|
|
||||||
### Unit тесты
|
|
||||||
- Используют моки для изоляции тестируемого кода
|
|
||||||
- Проверяют логику методов без зависимости от БД
|
|
||||||
- Быстрые и надежные
|
|
||||||
|
|
||||||
### Интеграционные тесты
|
|
||||||
- Используют реальную SQLite БД в памяти
|
|
||||||
- Проверяют взаимодействие с БД
|
|
||||||
- Создают временные файлы БД для каждого теста
|
|
||||||
- Автоматически очищают ресурсы после тестов
|
|
||||||
|
|
||||||
### Фикстуры
|
|
||||||
- Переиспользуемые объекты для тестов
|
|
||||||
- Автоматическая очистка ресурсов
|
|
||||||
- Разделение на unit и integration фикстуры
|
|
||||||
|
|
||||||
## Покрытие тестами
|
|
||||||
|
|
||||||
Тесты покрывают все публичные методы `PostRepository`:
|
|
||||||
|
|
||||||
- ✅ `create_tables()` - создание таблиц БД
|
|
||||||
- ✅ `add_post()` - добавление поста
|
|
||||||
- ✅ `update_helper_message()` - обновление helper сообщения
|
|
||||||
- ✅ `add_post_content()` - добавление контента поста
|
|
||||||
- ✅ `get_post_content_by_helper_id()` - получение контента по helper ID
|
|
||||||
- ✅ `get_post_text_by_helper_id()` - получение текста поста по helper ID
|
|
||||||
- ✅ `get_post_ids_by_helper_id()` - получение ID сообщений по helper ID
|
|
||||||
- ✅ `get_author_id_by_message_id()` - получение ID автора по message ID
|
|
||||||
- ✅ `get_author_id_by_helper_message_id()` - получение ID автора по helper message ID
|
|
||||||
|
|
||||||
## Добавление новых тестов
|
|
||||||
|
|
||||||
При добавлении новых методов в `PostRepository`:
|
|
||||||
|
|
||||||
1. Добавьте unit тест в `test_post_repository.py`
|
|
||||||
2. Добавьте интеграционный тест в `test_post_repository_integration.py`
|
|
||||||
3. Добавьте необходимые фикстуры в `conftest_post_repository.py`
|
|
||||||
4. Обновите этот README файл
|
|
||||||
|
|
||||||
## Отладка тестов
|
|
||||||
|
|
||||||
Для отладки тестов используйте:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pytest tests/test_post_repository.py -v -s --tb=long
|
|
||||||
```
|
|
||||||
|
|
||||||
Флаг `-s` позволяет видеть print statements, `--tb=long` показывает полный traceback ошибок.
|
|
||||||
Reference in New Issue
Block a user