diff --git a/src/dutylog/application/__main__.py b/src/dutylog/application/__main__.py index 059be71..c34a34e 100644 --- a/src/dutylog/application/__main__.py +++ b/src/dutylog/application/__main__.py @@ -13,6 +13,7 @@ from dutylog.application.bot.user_dialogs import main_menu_dialog from dutylog.application.bot.admin_dialogs import admin_menu_dialog from dutylog.application.bot.user_dialogs.registration_dialog import registration_dialog from dutylog.application.bot.user_dialogs.user_menu.feedback import feedback_router +from dutylog.application.bot.middlewares.album import AlbumMiddleware from dutylog.infrastructure.ioc import ( ConfigProvider, DatabaseProvider, @@ -37,6 +38,8 @@ async def main(): dp = Dispatcher() + dp.message.outer_middleware(AlbumMiddleware()) + container = make_async_container( ConfigProvider(), DatabaseProvider(), diff --git a/src/dutylog/application/bot/admin_dialogs/hours_management.py b/src/dutylog/application/bot/admin_dialogs/hours_management.py index 9f245d7..c398351 100644 --- a/src/dutylog/application/bot/admin_dialogs/hours_management.py +++ b/src/dutylog/application/bot/admin_dialogs/hours_management.py @@ -1,4 +1,3 @@ -import asyncio import json from aiogram import Bot @@ -21,7 +20,6 @@ from dutylog.infrastructure.database.repositories.hours_transactions_repository from dutylog.infrastructure.database.repositories.users_repository import ( UsersRepository, ) -from dutylog.infrastructure.ioc import MediaGroupCollector async def on_add_hours_click( @@ -100,31 +98,22 @@ async def on_custom_hours_input( await message.answer("⚠️ Пожалуйста, введите корректное число") -@inject async def on_add_hours_remark_input( message: Message, widget: MessageInput, dialog_manager: DialogManager, - media_group_collector: FromDishka[MediaGroupCollector], ): - if message.media_group_id: - chat_id = message.chat.id - mgid = message.media_group_id - my_count = media_group_collector.add_photo(chat_id, mgid, message.photo[-1].file_id) - if message.caption and message.caption.strip(): - media_group_collector.set_remark(chat_id, mgid, message.caption.strip()) - await asyncio.sleep(2.0) - if media_group_collector.is_last(chat_id, mgid, my_count): - file_ids, remark = media_group_collector.pop(chat_id, mgid) - dialog_manager.dialog_data["hours_remark"] = remark - dialog_manager.dialog_data["photo_file_ids"] = file_ids - await dialog_manager.switch_to(AdminMenuSG.add_hours_confirm) - return - + album = dialog_manager.middleware_data.get("album") photo_file_ids = [] remark = None - if message.photo: + if album: + for msg in album: + if msg.photo: + photo_file_ids.append(msg.photo[-1].file_id) + if msg.caption and msg.caption.strip(): + remark = msg.caption.strip() + elif message.photo: photo_file_ids = [message.photo[-1].file_id] remark = message.caption.strip() if message.caption and message.caption.strip() else None elif message.text and len(message.text.strip()) > 0: @@ -139,31 +128,22 @@ async def on_add_hours_remark_input( await dialog_manager.switch_to(AdminMenuSG.add_hours_confirm) -@inject async def on_remove_hours_remark_input( message: Message, widget: MessageInput, dialog_manager: DialogManager, - media_group_collector: FromDishka[MediaGroupCollector], ): - if message.media_group_id: - chat_id = message.chat.id - mgid = message.media_group_id - my_count = media_group_collector.add_photo(chat_id, mgid, message.photo[-1].file_id) - if message.caption and message.caption.strip(): - media_group_collector.set_remark(chat_id, mgid, message.caption.strip()) - await asyncio.sleep(2.0) - if media_group_collector.is_last(chat_id, mgid, my_count): - file_ids, remark = media_group_collector.pop(chat_id, mgid) - dialog_manager.dialog_data["hours_remark"] = remark - dialog_manager.dialog_data["photo_file_ids"] = file_ids - await dialog_manager.switch_to(AdminMenuSG.remove_hours_confirm) - return - + album = dialog_manager.middleware_data.get("album") photo_file_ids = [] remark = None - if message.photo: + if album: + for msg in album: + if msg.photo: + photo_file_ids.append(msg.photo[-1].file_id) + if msg.caption and msg.caption.strip(): + remark = msg.caption.strip() + elif message.photo: photo_file_ids = [message.photo[-1].file_id] remark = message.caption.strip() if message.caption and message.caption.strip() else None elif message.text and len(message.text.strip()) > 0: @@ -239,13 +219,14 @@ async def on_add_hours_confirm( notification_text = f"
➕ Начислены часы\n\nКоличество:
{hours} ч\nПричина: {remark}\nАдминистратор: {admin_username}\n\nВсего неотработанных часов: {resident.active_hours} ч"
try:
- await bot.send_message(resident.user_entity, notification_text)
if photo_file_ids:
if len(photo_file_ids) == 1:
- await bot.send_photo(resident.user_entity, photo_file_ids[0])
+ await bot.send_photo(resident.user_entity, photo_file_ids[0], caption=notification_text)
else:
- media = [InputMediaPhoto(media=fid) for fid in photo_file_ids]
+ media = [InputMediaPhoto(media=fid, caption=notification_text if i == 0 else None) for i, fid in enumerate(photo_file_ids)]
await bot.send_media_group(resident.user_entity, media=media)
+ else:
+ await bot.send_message(resident.user_entity, notification_text)
except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
pass
@@ -304,13 +285,14 @@ async def on_remove_hours_confirm(
notification_text += f"Администратор: {admin_username}\n\nОсталось неотработанных часов: {resident.active_hours} ч"
try:
- await bot.send_message(resident.user_entity, notification_text)
if photo_file_ids:
if len(photo_file_ids) == 1:
- await bot.send_photo(resident.user_entity, photo_file_ids[0])
+ await bot.send_photo(resident.user_entity, photo_file_ids[0], caption=notification_text)
else:
- media = [InputMediaPhoto(media=fid) for fid in photo_file_ids]
+ media = [InputMediaPhoto(media=fid, caption=notification_text if i == 0 else None) for i, fid in enumerate(photo_file_ids)]
await bot.send_media_group(resident.user_entity, media=media)
+ else:
+ await bot.send_message(resident.user_entity, notification_text)
except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
pass
diff --git a/src/dutylog/application/bot/admin_dialogs/rooms_management.py b/src/dutylog/application/bot/admin_dialogs/rooms_management.py
index 439142f..63f36f7 100644
--- a/src/dutylog/application/bot/admin_dialogs/rooms_management.py
+++ b/src/dutylog/application/bot/admin_dialogs/rooms_management.py
@@ -1,3 +1,5 @@
+import json
+
from aiogram import Bot
from aiogram.enums import ContentType
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter
@@ -314,11 +316,24 @@ async def on_room_remark_input(
widget: MessageInput,
dialog_manager: DialogManager,
):
- if message.text and message.text.strip():
- dialog_manager.dialog_data["remark"] = message.text.strip()
- else:
- dialog_manager.dialog_data["remark"] = None
-
+ album = dialog_manager.middleware_data.get("album")
+ photo_file_ids = []
+ remark = None
+
+ if album:
+ for msg in album:
+ if msg.photo:
+ photo_file_ids.append(msg.photo[-1].file_id)
+ if msg.caption and msg.caption.strip():
+ remark = msg.caption.strip()
+ elif message.photo:
+ photo_file_ids = [message.photo[-1].file_id]
+ remark = message.caption.strip() if message.caption and message.caption.strip() else None
+ elif message.text and message.text.strip():
+ remark = message.text.strip()
+
+ dialog_manager.dialog_data["remark"] = remark
+ dialog_manager.dialog_data["photo_file_ids"] = photo_file_ids
await dialog_manager.switch_to(AdminMenuSG.room_add_hours_confirm)
@@ -328,6 +343,7 @@ async def on_room_skip_remark(
dialog_manager: DialogManager,
):
dialog_manager.dialog_data["remark"] = None
+ dialog_manager.dialog_data["photo_file_ids"] = []
await dialog_manager.switch_to(AdminMenuSG.room_add_hours_confirm)
@@ -338,10 +354,13 @@ async def get_room_hours_confirm_data(
hours = dialog_manager.dialog_data.get("selected_hours", 0)
remark = dialog_manager.dialog_data.get("remark", "")
remark_text = f"\n\nПримечание: {remark}" if remark else ""
+ photo_file_ids = dialog_manager.dialog_data.get("photo_file_ids", [])
+ has_photo = bool(photo_file_ids)
return {
"hours": hours,
"remark_text": remark_text,
+ "has_photo": f"✅ Да ({len(photo_file_ids)} шт.)" if has_photo else "❌ Нет",
}
@@ -363,12 +382,15 @@ async def on_room_add_hours_confirm(
admin_id = callback.from_user.id
if room_id and hours:
+ photo_file_ids = dialog_manager.dialog_data.get("photo_file_ids", [])
+ photo_file_id = json.dumps(photo_file_ids) if photo_file_ids else None
results = await transactions_repository.add_hours_to_room(
room_id=room_id,
amount=hours,
admin_id=admin_id,
is_active=True,
remark=remark,
+ photo_file_id=photo_file_id,
)
for transaction, resident in results:
@@ -377,11 +399,15 @@ async def on_room_add_hours_confirm(
if user:
try:
remark_text = f"\n💬 {remark}" if remark else ""
- await bot.send_message(
- user.id,
- f"� Уведомление\n\n" - f"Вашей комнате начислено +{hours} ч{remark_text}" - ) + notification_text = f"➕ Уведомление\n\nВашей комнате начислено +{hours} ч{remark_text}" + if photo_file_ids: + if len(photo_file_ids) == 1: + await bot.send_photo(user.id, photo_file_ids[0], caption=notification_text) + else: + media = [InputMediaPhoto(media=fid, caption=notification_text if i == 0 else None) for i, fid in enumerate(photo_file_ids)] + await bot.send_media_group(user.id, media=media) + else: + await bot.send_message(user.id, notification_text) except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter): pass @@ -703,7 +729,7 @@ room_add_hours_custom_window = Window( ) room_add_hours_remark_window = Window( - Const("💬 Примечание\n\nВведите примечание к операции (или пропустите):"), + Const("💬 Примечание\n\nВведите примечание к операции или отправьте фото/медиагруппу с подписью (или пропустите):"), MessageInput(on_room_remark_input), Button( Const("⏭ Пропустить"), @@ -719,7 +745,7 @@ room_add_hours_remark_window = Window( ) room_add_hours_confirm_window = Window( - Format("➕ Подтверждение\n\nВы уверены, что хотите добавить{hours}часов?{remark_text}"), + Format("➕ Подтверждение\n\nВы уверены, что хотите добавить{hours}часов?{remark_text}\n\n📷 Фото: {has_photo}"), Row( Button( Const("✅ Да"), diff --git a/src/dutylog/application/bot/middlewares/__init__.py b/src/dutylog/application/bot/middlewares/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/dutylog/application/bot/middlewares/album.py b/src/dutylog/application/bot/middlewares/album.py new file mode 100644 index 0000000..9564f66 --- /dev/null +++ b/src/dutylog/application/bot/middlewares/album.py @@ -0,0 +1,31 @@ +import asyncio +from collections.abc import Awaitable, Callable +from typing import Any + +from aiogram import BaseMiddleware +from aiogram.types import Message + + +class AlbumMiddleware(BaseMiddleware): + ALBUM_DATA: dict[str, list[Message]] = {} + + def __init__(self, delay: float = 0.6) -> None: + self.delay = delay + + async def __call__( + self, + handler: Callable[[Message, dict[str, Any]], Awaitable[Any]], + event: Message, + data: dict[str, Any], + ) -> Any: + if not event.media_group_id: + return await handler(event, data) + + try: + self.ALBUM_DATA[event.media_group_id].append(event) + return + except KeyError: + self.ALBUM_DATA[event.media_group_id] = [event] + await asyncio.sleep(self.delay) + data["album"] = self.ALBUM_DATA.pop(event.media_group_id) + return await handler(event, data) diff --git a/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py b/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py index 99e97ea..2c3af27 100644 --- a/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py +++ b/src/dutylog/infrastructure/database/repositories/hours_transactions_repository.py @@ -154,6 +154,7 @@ class HoursTransactionsRepository: admin_id: int | None = None, is_active: bool = True, remark: str | None = None, + photo_file_id: str | None = None, ) -> list[tuple[HoursTransaction, Resident | None]]: residents = await self.residents_dao.get_by_room(room_id) results = [] @@ -171,6 +172,7 @@ class HoursTransactionsRepository: is_active=is_active, remark=remark, per_room=True, + photo_file_id=photo_file_id, ) results.append(result) diff --git a/src/dutylog/infrastructure/ioc.py b/src/dutylog/infrastructure/ioc.py index 2090816..8c6661f 100644 --- a/src/dutylog/infrastructure/ioc.py +++ b/src/dutylog/infrastructure/ioc.py @@ -36,41 +36,11 @@ from dutylog.infrastructure.utils.config import Config, load_config from dutylog.services.report_service import ReportService -class MediaGroupCollector: - def __init__(self) -> None: - self._photos: dict[tuple[int, str], list[str]] = {} - self._remarks: dict[tuple[int, str], str] = {} - self._counters: dict[tuple[int, str], int] = {} - - def add_photo(self, chat_id: int, media_group_id: str, file_id: str) -> int: - key = (chat_id, media_group_id) - self._counters[key] = self._counters.get(key, 0) + 1 - self._photos.setdefault(key, []).append(file_id) - return self._counters[key] - - def set_remark(self, chat_id: int, media_group_id: str, remark: str) -> None: - self._remarks[(chat_id, media_group_id)] = remark - - def is_last(self, chat_id: int, media_group_id: str, my_count: int) -> bool: - return self._counters.get((chat_id, media_group_id), 0) == my_count - - def pop(self, chat_id: int, media_group_id: str) -> tuple[list[str], str | None]: - key = (chat_id, media_group_id) - file_ids = self._photos.pop(key, []) - remark = self._remarks.pop(key, None) - self._counters.pop(key, None) - return file_ids, remark - - class ConfigProvider(Provider): @provide(scope=Scope.APP) def get_config(self) -> Config: return load_config() - @provide(scope=Scope.APP) - def get_media_group_collector(self) -> MediaGroupCollector: - return MediaGroupCollector() - class DatabaseProvider(Provider): @provide(scope=Scope.APP)