Dev 11 #13

Merged
KerradKerridi merged 5 commits from dev-11 into master 2026-01-25 13:22:31 +00:00
2 changed files with 135 additions and 27 deletions
Showing only changes of commit 5a90591564 - Show all commits

View File

@@ -157,14 +157,42 @@ 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"""
await self.user_service.update_user_activity(message.from_user.id) # Проверяем, есть ли механизм для получения полной медиагруппы (для медиагрупп)
if message.media_group_id is None: album_getter = kwargs.get("album_getter")
await self.user_service.log_user_message(message)
await self.post_service.process_post(message, album) if album_getter and message.media_group_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') markup_for_user = await get_reply_keyboard(self.db, message.from_user.id)
await message.answer(success_send_message, reply_markup=markup_for_user) success_send_message = messages.get_message(get_first_name(message), 'SUCCESS_SEND_MESSAGE')
await state.set_state(FSM_STATES["START"]) await message.answer(success_send_message, reply_markup=markup_for_user)
await state.set_state(FSM_STATES["START"])
# В фоне ждем полную медиагруппу и обрабатываем пост
async def process_media_group_background():
try:
# Ждем полную медиагруппу
full_album = await album_getter.get_album(timeout=10.0)
if not full_album:
return
# Обрабатываем пост с полной медиагруппой
await self.user_service.update_user_activity(message.from_user.id)
await self.post_service.process_post(message, full_album)
except Exception as e:
from logs.custom_logger import logger
logger.error(f"Ошибка при фоновой обработке медиагруппы: {e}")
asyncio.create_task(process_media_group_background())
else:
# Обычное сообщение или медиагруппа уже собрана - обрабатываем синхронно
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.post_service.process_post(message, album)
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')
await message.answer(success_send_message, reply_markup=markup_for_user)
await state.set_state(FSM_STATES["START"])
@error_handler @error_handler
@track_errors("private_handlers", "stickers") @track_errors("private_handlers", "stickers")

View File

@@ -1,14 +1,42 @@
import asyncio import asyncio
from typing import Any, Dict, Union, List from typing import Any, Dict, Union, List, Optional
from aiogram import BaseMiddleware from aiogram import BaseMiddleware
from aiogram.types import Message from aiogram.types import Message
class AlbumGetter:
"""Вспомогательный класс для получения полной медиагруппы из middleware"""
def __init__(self, album_data: Dict[str, Any], media_group_id: str, event: asyncio.Event):
self.album_data = album_data
self.media_group_id = media_group_id
self.event = event
async def get_album(self, timeout: float = 10.0) -> Optional[List[Message]]:
"""
Ждет полную медиагруппу и возвращает ее.
Args:
timeout: Максимальное время ожидания в секундах
Returns:
Список сообщений медиагруппы или None при таймауте
"""
try:
await asyncio.wait_for(self.event.wait(), timeout=timeout)
if self.media_group_id in self.album_data:
return self.album_data[self.media_group_id].get("collected_album")
return None
except asyncio.TimeoutError:
return None
class AlbumMiddleware(BaseMiddleware): class AlbumMiddleware(BaseMiddleware):
""" """
Middleware для обработки медиа групп в Telegram. Middleware для обработки медиа групп в Telegram.
Собирает все сообщения одной медиа группы и передает их как album в data. Собирает все сообщения одной медиа группы и передает их как album в data.
Не блокирует handler - сразу вызывает его, а полную медиагруппу передает через Event.
""" """
def __init__(self, latency: Union[int, float] = 5.0): def __init__(self, latency: Union[int, float] = 5.0):
@@ -20,7 +48,8 @@ class AlbumMiddleware(BaseMiddleware):
""" """
super().__init__() super().__init__()
self.latency = latency self.latency = latency
self.album_data: Dict[str, Dict[str, List[Message]]] = {} # Храним данные медиагруппы: messages, event для уведомления, task для сбора
self.album_data: Dict[str, Dict[str, Any]] = {}
def collect_album_messages(self, event: Message) -> int: def collect_album_messages(self, event: Message) -> int:
""" """
@@ -41,12 +70,53 @@ class AlbumMiddleware(BaseMiddleware):
self.album_data[event.media_group_id]["messages"].append(event) self.album_data[event.media_group_id]["messages"].append(event)
return len(self.album_data[event.media_group_id]["messages"]) return len(self.album_data[event.media_group_id]["messages"])
async def _collect_album_background(self, media_group_id: str) -> None:
"""
Фоновая задача для сбора всех сообщений медиагруппы.
Args:
media_group_id: ID медиагруппы для сбора
"""
try:
await asyncio.sleep(self.latency)
if media_group_id not in self.album_data:
return
# Получаем текущий список сообщений
album_messages = self.album_data[media_group_id]["messages"].copy()
album_messages.sort(key=lambda x: x.message_id)
# Сохраняем собранную медиагруппу и уведомляем через Event
self.album_data[media_group_id]["collected_album"] = album_messages
self.album_data[media_group_id]["event"].set()
# Очищаем данные после небольшой задержки (чтобы handler успел получить album)
await asyncio.sleep(1.0)
if media_group_id in self.album_data:
task = self.album_data[media_group_id].get("task")
if task and not task.done():
task.cancel()
del self.album_data[media_group_id]
except Exception:
# В случае ошибки все равно уведомляем, чтобы handler не завис
if media_group_id in self.album_data:
self.album_data[media_group_id]["event"].set()
# Очищаем данные даже при ошибке
try:
task = self.album_data[media_group_id].get("task")
if task and not task.done():
task.cancel()
del self.album_data[media_group_id]
except Exception:
pass
async def __call__(self, handler, event: Message, data: Dict[str, Any]) -> Any: async def __call__(self, handler, event: Message, data: Dict[str, Any]) -> Any:
""" """
Основная логика middleware. Основная логика middleware.
Собирает все сообщения медиагруппы и обрабатывает только последнее сообщение Для медиагрупп: сразу вызывает handler, передавая Event для получения полной медиагруппы.
после завершения сбора всех сообщений. Для обычных сообщений: сразу вызывает handler.
Args: Args:
handler: Обработчик события handler: Обработчик события
@@ -62,26 +132,36 @@ class AlbumMiddleware(BaseMiddleware):
media_group_id = event.media_group_id media_group_id = event.media_group_id
message_id = event.message_id message_id = event.message_id
# Если это первое сообщение медиагруппы - создаем структуру данных
is_first_message = False
if media_group_id not in self.album_data: if media_group_id not in self.album_data:
self.album_data[media_group_id] = {"messages": []} is_first_message = True
album_event = asyncio.Event()
self.album_data[media_group_id] = {
"messages": [],
"event": album_event,
"task": None,
"first_message_id": message_id
}
# Запускаем фоновую задачу для сбора медиагруппы
task = asyncio.create_task(self._collect_album_background(media_group_id))
self.album_data[media_group_id]["task"] = task
# Добавляем сообщение в медиагруппу
self.album_data[media_group_id]["messages"].append(event) self.album_data[media_group_id]["messages"].append(event)
count_before = len(self.album_data[media_group_id]["messages"])
await asyncio.sleep(self.latency) # Обрабатываем только первое сообщение медиагруппы
if not is_first_message:
count_after = len(self.album_data[media_group_id]["messages"]) # Для остальных сообщений просто возвращаемся, не вызывая handler
if count_before != count_after:
return return
album_messages = self.album_data[media_group_id]["messages"] # Передаем объект-геттер в data, чтобы handler мог получить полную медиагруппу
album_messages.sort(key=lambda x: x.message_id) album_getter = AlbumGetter(
last_message_id = album_messages[-1].message_id self.album_data,
media_group_id,
if message_id != last_message_id: self.album_data[media_group_id]["event"]
return )
data["album_getter"] = album_getter
data["album"] = album_messages
del self.album_data[media_group_id]
# Сразу вызываем handler только для первого сообщения (не блокируем)
return await handler(event, data) return await handler(event, data)