from __future__ import annotations import logging import mimetypes from typing import Any from aiogram import Bot from aiogram.client.default import DefaultBotProperties from aiogram.enums import ParseMode from aiogram.types import BufferedInputFile from fastapi import ( APIRouter, File, Form, HTTPException, Request, Response, Security, UploadFile, status, ) from app.config import settings from ..dependencies import require_api_token from ..schemas.media import MediaUploadResponse router = APIRouter() logger = logging.getLogger(__name__) ALLOWED_MEDIA_TYPES = {"photo", "video", "document"} def _resolve_target_chat_id() -> int: """Выбирает чат для загрузки файлов (канал уведомлений или первый админ).""" chat_id = settings.get_admin_notifications_chat_id() if chat_id is not None: return chat_id admin_ids = settings.get_admin_ids() if admin_ids: return admin_ids[0] raise HTTPException( status.HTTP_500_INTERNAL_SERVER_ERROR, "Не настроен чат для загрузки файлов (ADMIN_NOTIFICATIONS_CHAT_ID или ADMIN_IDS)", ) def _build_media_url(request: Request, file_id: str) -> str: return str(request.url_for("download_media", file_id=file_id)) @router.post("/upload", response_model=MediaUploadResponse, tags=["media"], status_code=status.HTTP_201_CREATED) async def upload_media( request: Request, _: Any = Security(require_api_token), file: UploadFile = File(...), media_type: str = Form("document", description="Тип файла: photo, video или document"), caption: str | None = Form(None, description="Необязательная подпись к файлу"), ) -> MediaUploadResponse: media_type_normalized = (media_type or "").strip().lower() if media_type_normalized not in ALLOWED_MEDIA_TYPES: raise HTTPException(status.HTTP_400_BAD_REQUEST, "Unsupported media type") file_bytes = await file.read() if not file_bytes: raise HTTPException(status.HTTP_400_BAD_REQUEST, "File is empty") target_chat_id = _resolve_target_chat_id() upload = BufferedInputFile(file_bytes, filename=file.filename or "upload") bot = Bot( token=settings.BOT_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML), ) try: if media_type_normalized == "photo": message = await bot.send_photo( chat_id=target_chat_id, photo=upload, caption=caption, ) media = message.photo[-1] elif media_type_normalized == "video": message = await bot.send_video( chat_id=target_chat_id, video=upload, caption=caption, ) media = message.video else: message = await bot.send_document( chat_id=target_chat_id, document=upload, caption=caption, ) media = message.document media_url = _build_media_url(request, media.file_id) return MediaUploadResponse( media_type=media_type_normalized, file_id=media.file_id, file_unique_id=getattr(media, "file_unique_id", None), media_url=media_url, ) except HTTPException: raise except Exception as error: logger.error("Failed to upload media: %s", error) raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to upload media") from error finally: await bot.session.close() @router.get("/media/{file_id}", name="download_media", tags=["media"]) async def download_media( file_id: str, _: Any = Security(require_api_token), ) -> Response: bot = Bot( token=settings.BOT_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML), ) try: file = await bot.get_file(file_id) if not file.file_path: raise HTTPException(status.HTTP_404_NOT_FOUND, "Media file not found") buffer = await bot.download_file(file.file_path) if hasattr(buffer, "seek"): buffer.seek(0) content = buffer.read() if hasattr(buffer, "read") else bytes(buffer) filename = file.file_path.split("/")[-1] media_type = mimetypes.guess_type(filename)[0] or "application/octet-stream" return Response( content=content, media_type=media_type, headers={ "Content-Disposition": f"inline; filename={filename}", }, ) except HTTPException: raise except Exception as error: # pragma: no cover - неожиданные ошибки загрузки файла logger.error("Failed to download media %s: %s", file_id, error) raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, "Failed to download media") from error finally: await bot.session.close()