Files
remnawave-bedolaga-telegram…/app/webapi/routes/media.py
T
c0mrade 9a2aea038a chore: add uv package manager and ruff linter configuration
- Add pyproject.toml with uv and ruff configuration
- Pin Python version to 3.13 via .python-version
- Add Makefile commands: lint, format, fix
- Apply ruff formatting to entire codebase
- Remove unused imports (base64 in yookassa/simple_subscription)
- Update .gitignore for new config files
2026-01-24 17:45:27 +03:00

158 lines
5.0 KiB
Python

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()