mirror of
https://github.com/koloideal/DutyLog.git
synced 2026-08-08 18:11:14 +03:00
refactor: use AlbumMiddleware instead of MediaGroupCollector, send photos with caption, add photo support for room hours
This commit is contained in:
@@ -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.admin_dialogs import admin_menu_dialog
|
||||||
from dutylog.application.bot.user_dialogs.registration_dialog import registration_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.user_dialogs.user_menu.feedback import feedback_router
|
||||||
|
from dutylog.application.bot.middlewares.album import AlbumMiddleware
|
||||||
from dutylog.infrastructure.ioc import (
|
from dutylog.infrastructure.ioc import (
|
||||||
ConfigProvider,
|
ConfigProvider,
|
||||||
DatabaseProvider,
|
DatabaseProvider,
|
||||||
@@ -37,6 +38,8 @@ async def main():
|
|||||||
|
|
||||||
dp = Dispatcher()
|
dp = Dispatcher()
|
||||||
|
|
||||||
|
dp.message.outer_middleware(AlbumMiddleware())
|
||||||
|
|
||||||
container = make_async_container(
|
container = make_async_container(
|
||||||
ConfigProvider(),
|
ConfigProvider(),
|
||||||
DatabaseProvider(),
|
DatabaseProvider(),
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
@@ -21,7 +20,6 @@ from dutylog.infrastructure.database.repositories.hours_transactions_repository
|
|||||||
from dutylog.infrastructure.database.repositories.users_repository import (
|
from dutylog.infrastructure.database.repositories.users_repository import (
|
||||||
UsersRepository,
|
UsersRepository,
|
||||||
)
|
)
|
||||||
from dutylog.infrastructure.ioc import MediaGroupCollector
|
|
||||||
|
|
||||||
|
|
||||||
async def on_add_hours_click(
|
async def on_add_hours_click(
|
||||||
@@ -100,31 +98,22 @@ async def on_custom_hours_input(
|
|||||||
await message.answer("⚠️ Пожалуйста, введите корректное число")
|
await message.answer("⚠️ Пожалуйста, введите корректное число")
|
||||||
|
|
||||||
|
|
||||||
@inject
|
|
||||||
async def on_add_hours_remark_input(
|
async def on_add_hours_remark_input(
|
||||||
message: Message,
|
message: Message,
|
||||||
widget: MessageInput,
|
widget: MessageInput,
|
||||||
dialog_manager: DialogManager,
|
dialog_manager: DialogManager,
|
||||||
media_group_collector: FromDishka[MediaGroupCollector],
|
|
||||||
):
|
):
|
||||||
if message.media_group_id:
|
album = dialog_manager.middleware_data.get("album")
|
||||||
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
|
|
||||||
|
|
||||||
photo_file_ids = []
|
photo_file_ids = []
|
||||||
remark = None
|
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]
|
photo_file_ids = [message.photo[-1].file_id]
|
||||||
remark = message.caption.strip() if message.caption and message.caption.strip() else None
|
remark = message.caption.strip() if message.caption and message.caption.strip() else None
|
||||||
elif message.text and len(message.text.strip()) > 0:
|
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)
|
await dialog_manager.switch_to(AdminMenuSG.add_hours_confirm)
|
||||||
|
|
||||||
|
|
||||||
@inject
|
|
||||||
async def on_remove_hours_remark_input(
|
async def on_remove_hours_remark_input(
|
||||||
message: Message,
|
message: Message,
|
||||||
widget: MessageInput,
|
widget: MessageInput,
|
||||||
dialog_manager: DialogManager,
|
dialog_manager: DialogManager,
|
||||||
media_group_collector: FromDishka[MediaGroupCollector],
|
|
||||||
):
|
):
|
||||||
if message.media_group_id:
|
album = dialog_manager.middleware_data.get("album")
|
||||||
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
|
|
||||||
|
|
||||||
photo_file_ids = []
|
photo_file_ids = []
|
||||||
remark = None
|
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]
|
photo_file_ids = [message.photo[-1].file_id]
|
||||||
remark = message.caption.strip() if message.caption and message.caption.strip() else None
|
remark = message.caption.strip() if message.caption and message.caption.strip() else None
|
||||||
elif message.text and len(message.text.strip()) > 0:
|
elif message.text and len(message.text.strip()) > 0:
|
||||||
@@ -239,13 +219,14 @@ async def on_add_hours_confirm(
|
|||||||
notification_text = f"<blockquote>➕ <b>Начислены часы</b></blockquote>\n\n<b>Количество:</b> <code>{hours}</code> ч\n<b>Причина:</b> {remark}\n<b>Администратор:</b> {admin_username}\n\n<b>Всего неотработанных часов:</b> <code>{resident.active_hours}</code> ч"
|
notification_text = f"<blockquote>➕ <b>Начислены часы</b></blockquote>\n\n<b>Количество:</b> <code>{hours}</code> ч\n<b>Причина:</b> {remark}\n<b>Администратор:</b> {admin_username}\n\n<b>Всего неотработанных часов:</b> <code>{resident.active_hours}</code> ч"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await bot.send_message(resident.user_entity, notification_text)
|
|
||||||
if photo_file_ids:
|
if photo_file_ids:
|
||||||
if len(photo_file_ids) == 1:
|
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:
|
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)
|
await bot.send_media_group(resident.user_entity, media=media)
|
||||||
|
else:
|
||||||
|
await bot.send_message(resident.user_entity, notification_text)
|
||||||
except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
|
except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -304,13 +285,14 @@ async def on_remove_hours_confirm(
|
|||||||
notification_text += f"<b>Администратор:</b> {admin_username}\n\n<b>Осталось неотработанных часов:</b> <code>{resident.active_hours}</code> ч"
|
notification_text += f"<b>Администратор:</b> {admin_username}\n\n<b>Осталось неотработанных часов:</b> <code>{resident.active_hours}</code> ч"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await bot.send_message(resident.user_entity, notification_text)
|
|
||||||
if photo_file_ids:
|
if photo_file_ids:
|
||||||
if len(photo_file_ids) == 1:
|
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:
|
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)
|
await bot.send_media_group(resident.user_entity, media=media)
|
||||||
|
else:
|
||||||
|
await bot.send_message(resident.user_entity, notification_text)
|
||||||
except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
|
except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
from aiogram.enums import ContentType
|
from aiogram.enums import ContentType
|
||||||
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter
|
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter
|
||||||
@@ -314,11 +316,24 @@ async def on_room_remark_input(
|
|||||||
widget: MessageInput,
|
widget: MessageInput,
|
||||||
dialog_manager: DialogManager,
|
dialog_manager: DialogManager,
|
||||||
):
|
):
|
||||||
if message.text and message.text.strip():
|
album = dialog_manager.middleware_data.get("album")
|
||||||
dialog_manager.dialog_data["remark"] = message.text.strip()
|
photo_file_ids = []
|
||||||
else:
|
remark = None
|
||||||
dialog_manager.dialog_data["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)
|
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: DialogManager,
|
||||||
):
|
):
|
||||||
dialog_manager.dialog_data["remark"] = None
|
dialog_manager.dialog_data["remark"] = None
|
||||||
|
dialog_manager.dialog_data["photo_file_ids"] = []
|
||||||
await dialog_manager.switch_to(AdminMenuSG.room_add_hours_confirm)
|
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)
|
hours = dialog_manager.dialog_data.get("selected_hours", 0)
|
||||||
remark = dialog_manager.dialog_data.get("remark", "")
|
remark = dialog_manager.dialog_data.get("remark", "")
|
||||||
remark_text = f"\n\n<b>Примечание:</b> {remark}" if remark else ""
|
remark_text = f"\n\n<b>Примечание:</b> {remark}" if remark else ""
|
||||||
|
photo_file_ids = dialog_manager.dialog_data.get("photo_file_ids", [])
|
||||||
|
has_photo = bool(photo_file_ids)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"hours": hours,
|
"hours": hours,
|
||||||
"remark_text": remark_text,
|
"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
|
admin_id = callback.from_user.id
|
||||||
|
|
||||||
if room_id and hours:
|
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(
|
results = await transactions_repository.add_hours_to_room(
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
amount=hours,
|
amount=hours,
|
||||||
admin_id=admin_id,
|
admin_id=admin_id,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
remark=remark,
|
remark=remark,
|
||||||
|
photo_file_id=photo_file_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
for transaction, resident in results:
|
for transaction, resident in results:
|
||||||
@@ -377,11 +399,15 @@ async def on_room_add_hours_confirm(
|
|||||||
if user:
|
if user:
|
||||||
try:
|
try:
|
||||||
remark_text = f"\n💬 <i>{remark}</i>" if remark else ""
|
remark_text = f"\n💬 <i>{remark}</i>" if remark else ""
|
||||||
await bot.send_message(
|
notification_text = f"<blockquote>➕ <b>Уведомление</b></blockquote>\n\nВашей комнате начислено <b>+{hours}</b> ч{remark_text}"
|
||||||
user.id,
|
if photo_file_ids:
|
||||||
f"<blockquote>� <b>Уведомление</b></blockq uote>\n\n"
|
if len(photo_file_ids) == 1:
|
||||||
f"Вашей комнате начислено <b>+{hours}</b> ч{remark_text}"
|
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):
|
except (TelegramBadRequest, TelegramForbiddenError, TelegramRetryAfter):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -703,7 +729,7 @@ room_add_hours_custom_window = Window(
|
|||||||
)
|
)
|
||||||
|
|
||||||
room_add_hours_remark_window = Window(
|
room_add_hours_remark_window = Window(
|
||||||
Const("<blockquote>💬 <b>Примечание</b></blockquote>\n\nВведите примечание к операции (или пропустите):"),
|
Const("<blockquote>💬 <b>Примечание</b></blockquote>\n\nВведите примечание к операции или отправьте фото/медиагруппу с подписью (или пропустите):"),
|
||||||
MessageInput(on_room_remark_input),
|
MessageInput(on_room_remark_input),
|
||||||
Button(
|
Button(
|
||||||
Const("⏭ Пропустить"),
|
Const("⏭ Пропустить"),
|
||||||
@@ -719,7 +745,7 @@ room_add_hours_remark_window = Window(
|
|||||||
)
|
)
|
||||||
|
|
||||||
room_add_hours_confirm_window = Window(
|
room_add_hours_confirm_window = Window(
|
||||||
Format("<blockquote>➕ <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите добавить <code>{hours}</code> часов?{remark_text}"),
|
Format("<blockquote>➕ <b>Подтверждение</b></blockquote>\n\nВы уверены, что хотите добавить <code>{hours}</code> часов?{remark_text}\n\n📷 <b>Фото:</b> {has_photo}"),
|
||||||
Row(
|
Row(
|
||||||
Button(
|
Button(
|
||||||
Const("✅ Да"),
|
Const("✅ Да"),
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -154,6 +154,7 @@ class HoursTransactionsRepository:
|
|||||||
admin_id: int | None = None,
|
admin_id: int | None = None,
|
||||||
is_active: bool = True,
|
is_active: bool = True,
|
||||||
remark: str | None = None,
|
remark: str | None = None,
|
||||||
|
photo_file_id: str | None = None,
|
||||||
) -> list[tuple[HoursTransaction, Resident | None]]:
|
) -> list[tuple[HoursTransaction, Resident | None]]:
|
||||||
residents = await self.residents_dao.get_by_room(room_id)
|
residents = await self.residents_dao.get_by_room(room_id)
|
||||||
results = []
|
results = []
|
||||||
@@ -171,6 +172,7 @@ class HoursTransactionsRepository:
|
|||||||
is_active=is_active,
|
is_active=is_active,
|
||||||
remark=remark,
|
remark=remark,
|
||||||
per_room=True,
|
per_room=True,
|
||||||
|
photo_file_id=photo_file_id,
|
||||||
)
|
)
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
|
||||||
|
|||||||
@@ -36,41 +36,11 @@ from dutylog.infrastructure.utils.config import Config, load_config
|
|||||||
from dutylog.services.report_service import ReportService
|
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):
|
class ConfigProvider(Provider):
|
||||||
@provide(scope=Scope.APP)
|
@provide(scope=Scope.APP)
|
||||||
def get_config(self) -> Config:
|
def get_config(self) -> Config:
|
||||||
return load_config()
|
return load_config()
|
||||||
|
|
||||||
@provide(scope=Scope.APP)
|
|
||||||
def get_media_group_collector(self) -> MediaGroupCollector:
|
|
||||||
return MediaGroupCollector()
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseProvider(Provider):
|
class DatabaseProvider(Provider):
|
||||||
@provide(scope=Scope.APP)
|
@provide(scope=Scope.APP)
|
||||||
|
|||||||
Reference in New Issue
Block a user