From 86c739b6960cdd1e664a9d5b6a3aa0fa19a474fc Mon Sep 17 00:00:00 2001 From: austnv Date: Tue, 16 Jun 2026 15:53:27 +0300 Subject: [PATCH] feat: add route to upload media and edit frontend component --- .../bot/app/web/admin_api_impl/broadcast.py | 69 + backend/bot/app/web/admin_api_impl/routes.py | 2 + .../admin/sections/BroadcastSection.svelte | 405 +++- .../src/lib/admin/stores/broadcastStore.js | 41 +- package-lock.json | 1666 +++++++++++++++++ package.json | 3 + 6 files changed, 2164 insertions(+), 22 deletions(-) create mode 100644 package-lock.json diff --git a/backend/bot/app/web/admin_api_impl/broadcast.py b/backend/bot/app/web/admin_api_impl/broadcast.py index 5138eda..b4878bb 100644 --- a/backend/bot/app/web/admin_api_impl/broadcast.py +++ b/backend/bot/app/web/admin_api_impl/broadcast.py @@ -7,6 +7,11 @@ from collections import defaultdict from bot.utils.ttl_cache import AsyncTTLCache +import tempfile +import os +from aiogram.types import InputFile, Message +from aiogram.exceptions import TelegramBadRequest + BROADCAST_TARGET_ACTIVE_NEVER_CONNECTED = "active_never_connected" BROADCAST_TARGETS = { @@ -261,3 +266,67 @@ async def admin_broadcast_audience_counts_route(request: web.Request) -> web.Res ) return _ok({"counts": counts}) + + +async def admin_upload_media_route(request: web.Request) -> web.Response: + """ + Эндпоинт для загрузки медиафайла администратором. + Принимает multipart/form-data с полем 'file'. + Возвращает file_id, полученный от Telegram Bot. + """ + actor_id = _require_admin_user_id(request) + + # Проверяем, что это multipart-запрос + if request.content_type and not request.content_type.startswith("multipart/form-data"): + return _error(400, "invalid_content_type") + + reader = await request.multipart() + field = await reader.next() + if field is None or field.name != "file": + return _error(400, "file_required") + + # Сохраняем файл во временный файл + with tempfile.NamedTemporaryFile(delete=False, suffix=".tmp") as tmp: + while True: + chunk = await field.read_chunk() + if not chunk: + break + tmp.write(chunk) + tmp_path = tmp.name + + try: + # Получаем бота из app + bot = request.app.get("bot") + if bot is None: + return _error(503, "bot_unavailable") + + # Отправляем файл в личный чат администратора, чтобы получить file_id + # Используем send_document, так как он подходит для любых файлов + with open(tmp_path, "rb") as f: + input_file = InputFile(f) + sent_msg: Message = await bot.send_document( + chat_id=actor_id, + document=input_file, + # Не отправляем лишний текст, чтобы не привлекать внимание + ) + + file_id = sent_msg.document.file_id + + # Удаляем сообщение, чтобы не засорять чат + await bot.delete_message(chat_id=actor_id, message_id=sent_msg.message_id) + + # Возвращаем file_id + return _ok({"file_id": file_id}) + + except TelegramBadRequest as e: + logger.warning("Failed to upload media to Telegram: %s", e) + return _error(400, f"telegram_error: {str(e)}") + except Exception as e: + logger.error("Unexpected error during media upload: %s", e, exc_info=True) + return _error(500, "upload_failed") + finally: + # Удаляем временный файл + try: + os.unlink(tmp_path) + except OSError: + pass \ No newline at end of file diff --git a/backend/bot/app/web/admin_api_impl/routes.py b/backend/bot/app/web/admin_api_impl/routes.py index 146a011..5dc88b2 100644 --- a/backend/bot/app/web/admin_api_impl/routes.py +++ b/backend/bot/app/web/admin_api_impl/routes.py @@ -64,6 +64,8 @@ def setup_admin_routes(app: web.Application) -> None: router.add_get("/api/admin/broadcast/audience-counts", admin_broadcast_audience_counts_route) router.add_post("/api/admin/broadcast", admin_broadcast_route) + router.add_post("/api/admin/upload-media", admin_upload_media_route) + router.add_post("/api/admin/sync", admin_sync_route) router.add_get("/api/admin/ads", admin_ads_list_route) diff --git a/frontend/src/admin/sections/BroadcastSection.svelte b/frontend/src/admin/sections/BroadcastSection.svelte index 02b3b61..7fbd977 100644 --- a/frontend/src/admin/sections/BroadcastSection.svelte +++ b/frontend/src/admin/sections/BroadcastSection.svelte @@ -1,9 +1,10 @@