Добавлен функционал для работы с медиагруппами и улучшена обработка сообщений

- Реализованы методы для добавления связи между постами и сообщениями в `PostRepository` и `AsyncBotDB`.
- Обновлены обработчики публикации постов для корректной работы с медиагруппами, включая удаление и уведомление авторов.
- Улучшена логика обработки сообщений в `AlbumMiddleware` для более эффективного сбора медиагрупп.
- Обновлены тесты для проверки нового функционала и обработки ошибок.
This commit is contained in:
2026-01-24 01:23:35 +03:00
parent fecac6091e
commit 0e2aef8c03
7 changed files with 199 additions and 138 deletions

View File

@@ -139,6 +139,10 @@ class AsyncBotDB:
"""Добавление контента поста.""" """Добавление контента поста."""
return await self.factory.posts.add_post_content(post_id, message_id, content_name, content_type) return await self.factory.posts.add_post_content(post_id, message_id, content_name, content_type)
async def add_message_link(self, post_id: int, message_id: int) -> bool:
"""Добавляет связь между post_id и message_id в таблицу message_link_to_content."""
return await self.factory.posts.add_message_link(post_id, message_id)
async def get_post_content_from_telegram_by_last_id(self, last_post_id: int) -> List[Tuple[str, str]]: async def get_post_content_from_telegram_by_last_id(self, last_post_id: int) -> List[Tuple[str, str]]:
"""Получает контент поста по helper_text_message_id.""" """Получает контент поста по helper_text_message_id."""
return await self.factory.posts.get_post_content_by_helper_id(last_post_id) return await self.factory.posts.get_post_content_by_helper_id(last_post_id)
@@ -175,6 +179,10 @@ class AsyncBotDB:
"""Получает ID сообщений по helper_text_message_id.""" """Получает ID сообщений по helper_text_message_id."""
return await self.factory.posts.get_post_ids_by_helper_id(last_post_id) return await self.factory.posts.get_post_ids_by_helper_id(last_post_id)
async def get_post_ids_by_helper_id(self, helper_message_id: int) -> List[int]:
"""Алиас для get_post_ids_from_telegram_by_last_id (используется callback-сервисом)."""
return await self.get_post_ids_from_telegram_by_last_id(helper_message_id)
async def get_author_id_by_message_id(self, message_id: int) -> Optional[int]: async def get_author_id_by_message_id(self, message_id: int) -> Optional[int]:
"""Получает ID автора по message_id.""" """Получает ID автора по message_id."""
return await self.factory.posts.get_author_id_by_message_id(message_id) return await self.factory.posts.get_author_id_by_message_id(message_id)

View File

@@ -27,9 +27,21 @@ class PostRepository(DatabaseConnection):
# Добавляем поле published_message_id если его нет (для существующих БД) # Добавляем поле published_message_id если его нет (для существующих БД)
try: try:
check_column_query = """
SELECT name FROM pragma_table_info('post_from_telegram_suggest')
WHERE name = 'published_message_id'
"""
existing_columns = await self._execute_query_with_result(check_column_query)
if not existing_columns:
await self._execute_query('ALTER TABLE post_from_telegram_suggest ADD COLUMN published_message_id INTEGER') await self._execute_query('ALTER TABLE post_from_telegram_suggest ADD COLUMN published_message_id INTEGER')
self.logger.info("Столбец published_message_id добавлен в post_from_telegram_suggest")
except Exception as e:
# Если проверка не удалась, пытаемся добавить столбец (может быть уже существует)
try:
await self._execute_query('ALTER TABLE post_from_telegram_suggest ADD COLUMN published_message_id INTEGER')
self.logger.info("Столбец published_message_id добавлен в post_from_telegram_suggest (fallback)")
except Exception: except Exception:
# Поле уже существует, игнорируем ошибку # Столбец уже существует, игнорируем ошибку
pass pass
# Таблица контента постов # Таблица контента постов
@@ -85,14 +97,15 @@ class PostRepository(DatabaseConnection):
# Преобразуем bool в int для SQLite (True -> 1, False -> 0, None -> None) # Преобразуем bool в int для SQLite (True -> 1, False -> 0, None -> None)
is_anonymous_int = None if post.is_anonymous is None else (1 if post.is_anonymous else 0) is_anonymous_int = None if post.is_anonymous is None else (1 if post.is_anonymous else 0)
# Используем INSERT OR IGNORE чтобы избежать ошибок при повторном создании
query = """ query = """
INSERT INTO post_from_telegram_suggest (message_id, text, author_id, created_at, status, is_anonymous) INSERT OR IGNORE INTO post_from_telegram_suggest (message_id, text, author_id, created_at, status, is_anonymous)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
""" """
params = (post.message_id, post.text, post.author_id, post.created_at, status, is_anonymous_int) params = (post.message_id, post.text, post.author_id, post.created_at, status, is_anonymous_int)
await self._execute_query(query, params) await self._execute_query(query, params)
self.logger.info(f"Пост добавлен: message_id={post.message_id}") self.logger.info(f"Пост добавлен (или уже существует): message_id={post.message_id}, text длина={len(post.text) if post.text else 0}, is_anonymous={is_anonymous_int}")
async def update_helper_message(self, message_id: int, helper_message_id: int) -> None: async def update_helper_message(self, message_id: int, helper_message_id: int) -> None:
"""Обновление helper сообщения.""" """Обновление helper сообщения."""
@@ -188,6 +201,18 @@ class PostRepository(DatabaseConnection):
self.logger.error(f"Ошибка при добавлении контента поста: {e}") self.logger.error(f"Ошибка при добавлении контента поста: {e}")
return False return False
async def add_message_link(self, post_id: int, message_id: int) -> bool:
"""Добавляет связь между post_id и message_id в таблицу message_link_to_content."""
try:
self.logger.info(f"Добавление связи: post_id={post_id}, message_id={message_id}")
link_query = "INSERT OR IGNORE INTO message_link_to_content (post_id, message_id) VALUES (?, ?)"
await self._execute_query(link_query, (post_id, message_id))
self.logger.info(f"Связь успешно добавлена: post_id={post_id}, message_id={message_id}")
return True
except Exception as e:
self.logger.error(f"Ошибка при добавлении связи post_id={post_id}, message_id={message_id}: {e}")
return False
async def get_post_content_by_helper_id(self, helper_message_id: int) -> List[Tuple[str, str]]: async def get_post_content_by_helper_id(self, helper_message_id: int) -> List[Tuple[str, str]]:
"""Получает контент поста по helper_text_message_id.""" """Получает контент поста по helper_text_message_id."""
query = """ query = """

View File

@@ -54,7 +54,12 @@ class PostPublishService:
@track_errors("post_publish_service", "publish_post") @track_errors("post_publish_service", "publish_post")
async def publish_post(self, call: CallbackQuery) -> None: async def publish_post(self, call: CallbackQuery) -> None:
"""Основной метод публикации поста""" """Основной метод публикации поста"""
# Проверяем, является ли сообщение частью медиагруппы # Проверяем, является ли сообщение helper-сообщением медиагруппы
if call.message.text == CONTENT_TYPE_MEDIA_GROUP:
await self._publish_media_group(call)
return
# Проверяем, является ли сообщение частью медиагруппы (для обратной совместимости)
if call.message.media_group_id: if call.message.media_group_id:
await self._publish_media_group(call) await self._publish_media_group(call)
return return
@@ -280,44 +285,42 @@ class PostPublishService:
@track_media_processing("media_group") @track_media_processing("media_group")
async def _publish_media_group(self, call: CallbackQuery) -> None: async def _publish_media_group(self, call: CallbackQuery) -> None:
"""Публикация медиагруппы""" """Публикация медиагруппы"""
logger.info(f"Начинаю публикацию медиагруппы. Helper message ID: {call.message.message_id}")
try: try:
# call.message.message_id - это ID helper сообщения
helper_message_id = call.message.message_id helper_message_id = call.message.message_id
# Получаем контент медиагруппы по helper_message_id media_group_message_ids = await self.db.get_post_ids_by_helper_id(helper_message_id)
logger.debug(f"Получаю контент медиагруппы для helper_message_id: {helper_message_id}") if not media_group_message_ids:
logger.error(f"_publish_media_group: Не найдены message_id медиагруппы для helper_message_id={helper_message_id}")
raise PublishError("Не найдены message_id медиагруппы в базе данных")
post_content = await self.db.get_post_content_by_helper_id(helper_message_id) post_content = await self.db.get_post_content_by_helper_id(helper_message_id)
if not post_content: if not post_content:
logger.error(f"Контент медиагруппы не найден в базе данных для helper_message_id: {helper_message_id}") logger.error(f"_publish_media_group: Контент медиагруппы не найден в базе данных для helper_message_id={helper_message_id}")
raise PublishError("Контент медиагруппы не найден в базе данных") raise PublishError("Контент медиагруппы не найден в базе данных")
# Получаем сырой текст и is_anonymous по helper_message_id
logger.debug(f"Получаю текст и is_anonymous поста для helper_message_id: {helper_message_id}")
raw_text, is_anonymous = await self.db.get_post_text_and_anonymity_by_helper_id(helper_message_id) raw_text, is_anonymous = await self.db.get_post_text_and_anonymity_by_helper_id(helper_message_id)
if raw_text is None: if raw_text is None:
raw_text = "" raw_text = ""
logger.debug(f"Текст поста получен: {'пустой' if not raw_text else f'длина: {len(raw_text)} символов'}, is_anonymous={is_anonymous}")
# Получаем 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) author_id = await self.db.get_author_id_by_helper_message_id(helper_message_id)
if not author_id: if not author_id:
logger.error(f"Автор не найден для медиагруппы {helper_message_id}") logger.error(f"_publish_media_group: Автор не найден для медиагруппы helper_message_id={helper_message_id}")
raise PostNotFoundError(f"Автор не найден для медиагруппы {helper_message_id}") raise PostNotFoundError(f"Автор не найден для медиагруппы {helper_message_id}")
logger.debug(f"ID автора получен: {author_id}")
# Получаем данные автора
user = await self.db.get_user_by_id(author_id) user = await self.db.get_user_by_id(author_id)
if not user: if not user:
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных") raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
# Формируем финальный текст с учетом is_anonymous
formatted_text = get_text_message(raw_text, user.first_name, user.username, is_anonymous) formatted_text = get_text_message(raw_text, user.first_name, user.username, is_anonymous)
logger.debug(f"Сформирован финальный текст: {'пустой' if not formatted_text else f'длина: {len(formatted_text)} символов'}")
# Отправляем медиагруппу в канал try:
logger.info(f"Отправляю медиагруппу в канал {self.main_public}") await self._get_bot(call.message).delete_messages(
chat_id=self.group_for_posts,
message_ids=media_group_message_ids
)
except Exception as e:
logger.warning(f"_publish_media_group: Ошибка при удалении медиагруппы из чата модерации: {e}")
sent_messages = await send_media_group_to_channel( sent_messages = await send_media_group_to_channel(
bot=self._get_bot(call.message), bot=self._get_bot(call.message),
chat_id=self.main_public, chat_id=self.main_public,
@@ -326,31 +329,49 @@ class PostPublishService:
s3_storage=self.s3_storage s3_storage=self.s3_storage
) )
# Получаем оригинальные message_id из медиагруппы if len(sent_messages) == len(media_group_message_ids):
original_message_ids = await self.db.get_post_ids_from_telegram_by_last_id(helper_message_id) for i, original_message_id in enumerate(media_group_message_ids):
logger.debug(f"Получены оригинальные message_id медиагруппы: {original_message_ids}")
# Сохраняем published_message_id для каждого сообщения медиагруппы
if len(sent_messages) == len(original_message_ids):
for i, original_message_id in enumerate(original_message_ids):
published_message_id = sent_messages[i].message_id published_message_id = sent_messages[i].message_id
try:
await self.db.update_published_message_id( await self.db.update_published_message_id(
original_message_id=original_message_id, original_message_id=original_message_id,
published_message_id=published_message_id published_message_id=published_message_id
) )
# Сохраняем медиафайл из опубликованного сообщения (используем уже сохраненный файл)
await self._save_published_post_content(sent_messages[i], published_message_id, original_message_id) await self._save_published_post_content(sent_messages[i], published_message_id, original_message_id)
logger.debug(f"Сохранен published_message_id: {original_message_id} -> {published_message_id}") except Exception as e:
logger.warning(f"_publish_media_group: Ошибка при сохранении published_message_id для {original_message_id}: {e}")
else: else:
logger.warning(f"Количество опубликованных сообщений ({len(sent_messages)}) не совпадает с количеством оригинальных ({len(original_message_ids)})") logger.warning(f"_publish_media_group: Количество опубликованных сообщений ({len(sent_messages)}) не совпадает с количеством оригинальных ({len(media_group_message_ids)})")
await self.db.update_status_for_media_group_by_helper_id(helper_message_id, "approved") await self.db.update_status_for_media_group_by_helper_id(helper_message_id, "approved")
logger.debug(f"Удаляю медиагруппу и уведомляю автора {author_id}")
await self._delete_media_group_and_notify_author(call, author_id) # Удаляем helper сообщение - это критично, делаем это всегда
logger.info(f'Медиагруппа опубликована в канале {self.main_public}, опубликовано сообщений: {len(sent_messages)}.') try:
await self._get_bot(call.message).delete_message(
chat_id=self.group_for_posts,
message_id=helper_message_id
)
except Exception as e:
logger.warning(f"_publish_media_group: Ошибка при удалении helper сообщения: {e}")
try:
await send_text_message(author_id, call.message, MESSAGE_POST_PUBLISHED)
except Exception as e:
if str(e) == ERROR_BOT_BLOCKED:
logger.warning(f"_publish_media_group: Пользователь {author_id} заблокировал бота")
raise UserBlockedBotError("Пользователь заблокировал бота")
logger.error(f"_publish_media_group: Ошибка при отправке уведомления автору: {e}")
except Exception as e: except Exception as e:
logger.error(f"Ошибка при публикации медиагруппы: {e}") logger.error(f"_publish_media_group: Ошибка при публикации медиагруппы: {e}")
# Пытаемся удалить helper сообщение даже при ошибке
try:
await self._get_bot(call.message).delete_message(
chat_id=self.group_for_posts,
message_id=call.message.message_id
)
except Exception as delete_error:
logger.warning(f"_publish_media_group: Не удалось удалить helper сообщение при ошибке: {delete_error}")
raise PublishError(f"Не удалось опубликовать медиагруппу: {str(e)}") raise PublishError(f"Не удалось опубликовать медиагруппу: {str(e)}")
@track_time("decline_post", "post_publish_service") @track_time("decline_post", "post_publish_service")
@@ -399,27 +420,32 @@ class PostPublishService:
@track_media_processing("media_group") @track_media_processing("media_group")
async def _decline_media_group(self, call: CallbackQuery) -> None: async def _decline_media_group(self, call: CallbackQuery) -> None:
"""Отклонение медиагруппы""" """Отклонение медиагруппы"""
await self.db.update_status_for_media_group_by_helper_id(call.message.message_id, "declined") helper_message_id = call.message.message_id
post_ids = await self.db.get_post_ids_from_telegram_by_last_id(call.message.message_id) await self.db.update_status_for_media_group_by_helper_id(helper_message_id, "declined")
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) media_group_message_ids = await self.db.get_post_ids_by_helper_id(helper_message_id)
logger.debug(f"ID автора медиагруппы получен: {author_id}")
logger.debug(f"Удаляю {len(message_ids)} сообщений из группы {self.group_for_posts}") message_ids_to_delete = media_group_message_ids.copy()
await self._get_bot(call.message).delete_messages(chat_id=self.group_for_posts, message_ids=message_ids) message_ids_to_delete.append(helper_message_id)
author_id = await self._get_author_id_for_media_group(helper_message_id)
try:
await self._get_bot(call.message).delete_messages(
chat_id=self.group_for_posts,
message_ids=message_ids_to_delete
)
except Exception as e:
logger.warning(f"_decline_media_group: Ошибка при удалении сообщений: {e}")
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} заблокировал бота") logger.warning(f"_decline_media_group: Пользователь {author_id} заблокировал бота")
raise UserBlockedBotError("Пользователь заблокировал бота") raise UserBlockedBotError("Пользователь заблокировал бота")
logger.error(f"Ошибка при отправке уведомления автору медиагруппы {author_id}: {e}") logger.error(f"_decline_media_group: Ошибка при отправке уведомления автору {author_id}: {e}")
raise raise
@track_time("_get_author_id", "post_publish_service") @track_time("_get_author_id", "post_publish_service")
@@ -477,12 +503,15 @@ class PostPublishService:
@track_errors("post_publish_service", "_delete_media_group_and_notify_author") @track_errors("post_publish_service", "_delete_media_group_and_notify_author")
@track_media_processing("media_group") @track_media_processing("media_group")
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:
"""Удаление медиагруппы и уведомление автора""" """Удаление медиагруппы и уведомление автора (legacy метод, используется для обратной совместимости)"""
post_ids = await self.db.get_post_ids_from_telegram_by_last_id(call.message.message_id) helper_message_id = call.message.message_id
#message_ids = post_ids.copy() media_group_message_ids = await self.db.get_post_ids_by_helper_id(helper_message_id)
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) message_ids_to_delete = media_group_message_ids.copy()
message_ids_to_delete.append(helper_message_id)
await self._get_bot(call.message).delete_messages(chat_id=self.group_for_posts, message_ids=message_ids_to_delete)
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:
@@ -538,7 +567,12 @@ class BanService:
@db_query_time("ban_user_from_post", "users", "mixed") @db_query_time("ban_user_from_post", "users", "mixed")
async def ban_user_from_post(self, call: CallbackQuery) -> None: async def ban_user_from_post(self, call: CallbackQuery) -> None:
"""Бан пользователя за спам""" """Бан пользователя за спам"""
# Если это helper-сообщение медиагруппы, используем специальный метод
if call.message.text == CONTENT_TYPE_MEDIA_GROUP:
author_id = await self.db.get_author_id_by_helper_message_id(call.message.message_id)
else:
author_id = await self.db.get_author_id_by_message_id(call.message.message_id) author_id = await self.db.get_author_id_by_message_id(call.message.message_id)
if not author_id: if not author_id:
raise UserNotFoundError(f"Автор не найден для сообщения {call.message.message_id}") raise UserNotFoundError(f"Автор не найден для сообщения {call.message.message_id}")

View File

@@ -51,9 +51,8 @@ class PrivateHandlers:
self.post_service = PostService(db, settings, s3_storage) self.post_service = PostService(db, settings, s3_storage)
self.sticker_service = StickerService(settings) self.sticker_service = StickerService(settings)
# Create router
self.router = Router() self.router = Router()
self.router.message.middleware(AlbumMiddleware()) self.router.message.middleware(AlbumMiddleware(latency=5.0))
self.router.message.middleware(BlacklistMiddleware()) self.router.message.middleware(BlacklistMiddleware())
# Register handlers # Register handlers
@@ -158,12 +157,10 @@ class PrivateHandlers:
@track_time("suggest_router", "private_handlers") @track_time("suggest_router", "private_handlers")
async def suggest_router(self, message: types.Message, state: FSMContext, album: list = None, **kwargs): async def suggest_router(self, message: types.Message, state: FSMContext, album: list = None, **kwargs):
"""Handle post submission in suggest state""" """Handle post submission in suggest state"""
# Post service operations with metrics
await self.user_service.update_user_activity(message.from_user.id) await self.user_service.update_user_activity(message.from_user.id)
if message.media_group_id is None:
await self.user_service.log_user_message(message) await self.user_service.log_user_message(message)
await self.post_service.process_post(message, album) await self.post_service.process_post(message, album)
# Send success message and return to start state
markup_for_user = await get_reply_keyboard(self.db, message.from_user.id) markup_for_user = await get_reply_keyboard(self.db, message.from_user.id)
success_send_message = messages.get_message(get_first_name(message), 'SUCCESS_SEND_MESSAGE') success_send_message = messages.get_message(get_first_name(message), 'SUCCESS_SEND_MESSAGE')
await message.answer(success_send_message, reply_markup=markup_for_user) await message.answer(success_send_message, reply_markup=markup_for_user)

View File

@@ -323,7 +323,6 @@ class PostService:
@track_media_processing("media_group") @track_media_processing("media_group")
async def handle_media_group_post(self, message: types.Message, album: list, first_name: str) -> None: async def handle_media_group_post(self, message: types.Message, album: list, first_name: str) -> None:
"""Handle media group post submission""" """Handle media group post submission"""
#TODO: Мне кажется тут какая-то дичь с одинаковыми переменными, в которых post_caption никуда не ведет
post_caption = " " post_caption = " "
raw_caption = "" raw_caption = ""
@@ -331,12 +330,17 @@ class PostService:
raw_caption = album[0].caption or "" raw_caption = album[0].caption or ""
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)
# Определяем анонимность на основе сырого caption
is_anonymous = determine_anonymity(raw_caption) is_anonymous = determine_anonymity(raw_caption)
media_group = await prepare_media_group_from_middlewares(album, post_caption)
media_group_message_ids = await send_media_group_message_to_private_chat(
self.settings.group_for_posts, message, media_group, self.db, None, self.s3_storage
)
main_post_id = media_group_message_ids[-1]
# Создаем основной пост для медиагруппы
main_post = TelegramPost( main_post = TelegramPost(
message_id=message.message_id, # ID основного сообщения медиагруппы message_id=main_post_id,
text=raw_caption, text=raw_caption,
author_id=message.from_user.id, author_id=message.from_user.id,
created_at=int(datetime.now().timestamp()), created_at=int(datetime.now().timestamp()),
@@ -344,32 +348,32 @@ class PostService:
) )
await self.db.add_post(main_post) await self.db.add_post(main_post)
# Отправляем медиагруппу в группу для модерации for msg_id in media_group_message_ids:
media_group = await prepare_media_group_from_middlewares(album, post_caption) await self.db.add_message_link(main_post_id, msg_id)
media_group_message_id = await send_media_group_message_to_private_chat(
self.settings.group_for_posts, message, media_group, self.db, main_post.message_id, self.s3_storage
)
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 = await send_text_message(self.settings.group_for_posts, message, "ВРУЧНУЮ ВЫКЛАДЫВАТЬ, ПОСЛЕ ВЫКЛАДКИ УДАЛИТЬ ОБА ПОСТА") helper_message = await send_text_message(
self.settings.group_for_posts,
message,
"^",
markup
)
helper_message_id = helper_message.message_id
# Создаем helper пост и связываем его с основным
helper_post = TelegramPost( helper_post = TelegramPost(
message_id=help_message.message_id, # ID helper сообщения message_id=helper_message_id,
text="^", # Специальный маркер для медиагруппы text="^",
author_id=message.from_user.id, author_id=message.from_user.id,
helper_text_message_id=main_post.message_id, # Ссылка на основной пост helper_text_message_id=main_post_id,
created_at=int(datetime.now().timestamp()) created_at=int(datetime.now().timestamp())
) )
await self.db.add_post(helper_post) await self.db.add_post(helper_post)
# Обновляем основной пост, чтобы он ссылался на helper
await self.db.update_helper_message( await self.db.update_helper_message(
message_id=main_post.message_id, message_id=main_post_id,
helper_message_id=help_message.message_id helper_message_id=helper_message_id
) )
@track_time("process_post", "post_service") @track_time("process_post", "post_service")
@@ -378,13 +382,8 @@ 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 "Без никнейма"
await send_text_message(
self.settings.group_for_logs, message,
f'Закинул медиагруппу, пользователь: имя - {first_name}, ник - {safe_username}'
)
await self.handle_media_group_post(message, album, first_name) await self.handle_media_group_post(message, album, first_name)
return return

View File

@@ -11,7 +11,7 @@ class AlbumMiddleware(BaseMiddleware):
Собирает все сообщения одной медиа группы и передает их как album в data. Собирает все сообщения одной медиа группы и передает их как album в data.
""" """
def __init__(self, latency: Union[int, float] = 0.01): def __init__(self, latency: Union[int, float] = 5.0):
""" """
Инициализация middleware. Инициализация middleware.
@@ -45,6 +45,9 @@ class AlbumMiddleware(BaseMiddleware):
""" """
Основная логика middleware. Основная логика middleware.
Собирает все сообщения медиагруппы и обрабатывает только последнее сообщение
после завершения сбора всех сообщений.
Args: Args:
handler: Обработчик события handler: Обработчик события
event: Событие (сообщение) event: Событие (сообщение)
@@ -53,30 +56,32 @@ class AlbumMiddleware(BaseMiddleware):
Returns: Returns:
Результат выполнения обработчика Результат выполнения обработчика
""" """
# Если у события нет 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)
# Собираем сообщения одной медиа группы media_group_id = event.media_group_id
total_before = self.collect_album_messages(event) message_id = event.message_id
if media_group_id not in self.album_data:
self.album_data[media_group_id] = {"messages": []}
self.album_data[media_group_id]["messages"].append(event)
count_before = len(self.album_data[media_group_id]["messages"])
# Ждем указанный период для сбора всех сообщений
await asyncio.sleep(self.latency) await asyncio.sleep(self.latency)
# Проверяем количество сообщений после задержки count_after = len(self.album_data[media_group_id]["messages"])
total_after = len(self.album_data[event.media_group_id]["messages"]) if count_before != count_after:
# Если за время задержки добавились новые сообщения, выходим
if total_before != total_after:
return return
# Сортируем сообщения по message_id и добавляем в data album_messages = self.album_data[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)
last_message_id = album_messages[-1].message_id
if message_id != last_message_id:
return
data["album"] = album_messages data["album"] = album_messages
del self.album_data[media_group_id]
# Удаляем медиа группу из отслеживания для освобождения памяти
del self.album_data[event.media_group_id]
# Вызываем оригинальный обработчик события
return await handler(event, data) return await handler(event, data)

View File

@@ -369,9 +369,7 @@ async def add_in_db_media_mediagroup(sent_message: List[types.Message], bot_db:
logger.warning("add_in_db_media_mediagroup: Пустая медиагруппа") logger.warning("add_in_db_media_mediagroup: Пустая медиагруппа")
return False return False
# Используем переданный main_post_id или ID последнего сообщения
post_id = main_post_id or sent_message[-1].message_id post_id = main_post_id or sent_message[-1].message_id
logger.debug(f"add_in_db_media_mediagroup: Обрабатываю медиагруппу из {len(sent_message)} сообщений, post_id: {post_id}")
processed_count = 0 processed_count = 0
failed_count = 0 failed_count = 0
@@ -381,7 +379,6 @@ async def add_in_db_media_mediagroup(sent_message: List[types.Message], bot_db:
content_type = None content_type = None
file_id = None file_id = None
# Определяем тип контента и file_id
if message.photo: if message.photo:
content_type = 'photo' content_type = 'photo'
file_id = message.photo[-1].file_id file_id = message.photo[-1].file_id
@@ -407,54 +404,40 @@ async def add_in_db_media_mediagroup(sent_message: List[types.Message], bot_db:
failed_count += 1 failed_count += 1
continue continue
logger.debug(f"add_in_db_media_mediagroup: Обрабатываю {content_type} в сообщении {i+1}/{len(sent_message)}")
# Получаем s3_storage если не передан
if s3_storage is None: if s3_storage is None:
bdf = get_global_instance() bdf = get_global_instance()
s3_storage = bdf.get_s3_storage() s3_storage = bdf.get_s3_storage()
# Скачиваем файл (в S3 или на локальный диск)
file_path = await download_file(message, file_id=file_id, content_type=content_type, s3_storage=s3_storage) file_path = await download_file(message, file_id=file_id, content_type=content_type, s3_storage=s3_storage)
if not file_path: if not file_path:
logger.error(f"add_in_db_media_mediagroup: Не удалось скачать файл {file_id} в сообщении {i+1}/{len(sent_message)}") logger.error(f"add_in_db_media_mediagroup: Не удалось скачать файл {file_id} в сообщении {i+1}/{len(sent_message)}")
failed_count += 1 failed_count += 1
continue continue
# Добавляем в базу данных
# Для медиагруппы используем post_id (основной пост) как message_id для контента,
# так как FOREIGN KEY требует существования message_id в post_from_telegram_suggest
success = await bot_db.add_post_content(post_id, post_id, file_path, content_type) success = await bot_db.add_post_content(post_id, post_id, file_path, content_type)
if not success: if not success:
logger.error(f"add_in_db_media_mediagroup: Не удалось добавить контент в БД для сообщения {i+1}/{len(sent_message)}") logger.error(f"add_in_db_media_mediagroup: Не удалось добавить контент в БД для сообщения {i+1}/{len(sent_message)}")
# Удаляем скачанный файл при ошибке БД (только если это локальный файл, не S3)
if file_path.startswith('files/'): if file_path.startswith('files/'):
try: try:
os.remove(file_path) os.remove(file_path)
logger.debug(f"add_in_db_media_mediagroup: Удален файл {file_path} после ошибки БД")
except Exception as e: except Exception as e:
logger.warning(f"add_in_db_media_mediagroup: Не удалось удалить файл {file_path}: {e}") logger.warning(f"add_in_db_media_mediagroup: Не удалось удалить файл {file_path}: {e}")
failed_count += 1 failed_count += 1
continue continue
processed_count += 1 processed_count += 1
logger.debug(f"add_in_db_media_mediagroup: Успешно обработано сообщение {i+1}/{len(sent_message)}")
except Exception as e: except Exception as e:
logger.error(f"add_in_db_media_mediagroup: Ошибка обработки сообщения {i+1}/{len(sent_message)}: {e}") logger.error(f"add_in_db_media_mediagroup: Ошибка обработки сообщения {i+1}/{len(sent_message)}: {e}")
failed_count += 1 failed_count += 1
continue continue
processing_time = time.time() - start_time
if processed_count == 0: if processed_count == 0:
logger.error(f"add_in_db_media_mediagroup: Не удалось обработать ни одного сообщения из медиагруппы {post_id}") logger.error(f"add_in_db_media_mediagroup: Не удалось обработать ни одного сообщения из медиагруппы {post_id}")
return False return False
if failed_count > 0: if failed_count > 0:
logger.warning(f"add_in_db_media_mediagroup: Обработано {processed_count}/{len(sent_message)} сообщений медиагруппы {post_id}, ошибок: {failed_count}") logger.warning(f"add_in_db_media_mediagroup: Обработано {processed_count}/{len(sent_message)} сообщений медиагруппы {post_id}, ошибок: {failed_count}")
else:
logger.info(f"add_in_db_media_mediagroup: Успешно обработана медиагруппа {post_id} - {processed_count} сообщений, время: {processing_time:.2f}с")
return failed_count == 0 return failed_count == 0
@@ -556,22 +539,32 @@ async def add_in_db_media(sent_message: types.Message, bot_db: Any, s3_storage =
@track_media_processing("media_group") @track_media_processing("media_group")
@db_query_time("send_media_group_message_to_private_chat", "posts", "insert") @db_query_time("send_media_group_message_to_private_chat", "posts", "insert")
async def send_media_group_message_to_private_chat(chat_id: int, message: types.Message, async def send_media_group_message_to_private_chat(chat_id: int, message: types.Message,
media_group: List, bot_db: Any, main_post_id: Optional[int] = None, s3_storage=None) -> int: media_group: List, bot_db: Any, main_post_id: Optional[int] = None, s3_storage=None) -> List[int]:
sent_message = await message.bot.send_media_group( """
Отправляет медиагруппу в чат и возвращает все message_id отправленных сообщений.
Args:
chat_id: ID чата для отправки
message: Оригинальное сообщение от пользователя
media_group: Список InputMedia объектов
bot_db: Экземпляр базы данных
main_post_id: ID основного поста в БД (опционально)
s3_storage: S3StorageService для сохранения медиа
Returns:
List[int]: Список всех message_id отправленных сообщений медиагруппы
"""
sent_messages = await message.bot.send_media_group(
chat_id=chat_id, chat_id=chat_id,
media=media_group, media=media_group,
) )
post = TelegramPost(
message_id=sent_message[-1].message_id, sent_message_ids = [msg.message_id for msg in sent_messages]
text=sent_message[-1].caption or "", main_message_id = sent_message_ids[-1]
author_id=message.from_user.id,
created_at=int(datetime.now().timestamp()) asyncio.create_task(_save_media_group_background(sent_messages, bot_db, main_message_id, s3_storage))
)
await bot_db.add_post(post) return sent_message_ids
# Сохраняем медиа в фоне, чтобы не блокировать ответ пользователю
asyncio.create_task(_save_media_group_background(sent_message, bot_db, main_post_id, s3_storage))
message_id = sent_message[-1].message_id
return message_id
@track_time("send_media_group_to_channel", "helper_func") @track_time("send_media_group_to_channel", "helper_func")
@track_errors("helper_func", "send_media_group_to_channel") @track_errors("helper_func", "send_media_group_to_channel")