Files
remnawave-bedolaga-telegram…/app/cabinet/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

197 lines
6.2 KiB
Python

"""Media upload/download routes for cabinet tickets."""
import logging
import mimetypes
from aiogram import Bot
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.types import BufferedInputFile
from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, Response, UploadFile, status
from pydantic import BaseModel
from app.config import settings
from app.database.models import User
from ..dependencies import get_current_cabinet_user
logger = logging.getLogger(__name__)
router = APIRouter(prefix='/media', tags=['Cabinet Media'])
ALLOWED_MEDIA_TYPES = {'photo', 'video', 'document'}
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
class MediaUploadResponse(BaseModel):
"""Response after successful media upload."""
media_type: str
file_id: str
file_unique_id: str | None = None
media_url: str
def _resolve_target_chat_id() -> int:
"""Get chat ID for uploading files (notification channel or first admin)."""
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_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='No chat configured for file uploads',
)
def _build_media_url(request: Request, file_id: str) -> str:
"""Build URL for downloading media."""
return str(request.url_for('cabinet_download_media', file_id=file_id))
@router.post('/upload', response_model=MediaUploadResponse, status_code=status.HTTP_201_CREATED)
async def upload_media(
request: Request,
user: User = Depends(get_current_cabinet_user),
file: UploadFile = File(...),
media_type: str = Form('photo', description='File type: photo, video, or document'),
):
"""
Upload media file for use in ticket messages.
Returns file_id that can be used when creating ticket or adding message.
"""
media_type_normalized = (media_type or '').strip().lower()
if media_type_normalized not in ALLOWED_MEDIA_TYPES:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f'Unsupported media type. Allowed: {", ".join(ALLOWED_MEDIA_TYPES)}',
)
# Read and validate file
file_bytes = await file.read()
if not file_bytes:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='File is empty',
)
if len(file_bytes) > MAX_FILE_SIZE:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f'File too large. Maximum size: {MAX_FILE_SIZE // 1024 // 1024}MB',
)
# Validate content type for photos
if media_type_normalized == 'photo':
allowed_image_types = {'image/jpeg', 'image/png', 'image/gif', 'image/webp'}
if file.content_type and file.content_type not in allowed_image_types:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail='Invalid image type. Allowed: JPEG, PNG, GIF, WebP',
)
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,
)
media = message.photo[-1]
elif media_type_normalized == 'video':
message = await bot.send_video(
chat_id=target_chat_id,
video=upload,
)
media = message.video
else:
message = await bot.send_document(
chat_id=target_chat_id,
document=upload,
)
media = message.document
media_url = _build_media_url(request, media.file_id)
logger.info(f'User {user.telegram_id} uploaded {media_type_normalized}: {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(f'Failed to upload media for user {user.telegram_id}: {error}')
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to upload media',
) from error
finally:
await bot.session.close()
@router.get('/{file_id}', name='cabinet_download_media')
async def download_media(
file_id: str,
) -> Response:
"""
Download media file by file_id.
Used to display images/documents in ticket messages.
"""
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_code=status.HTTP_404_NOT_FOUND,
detail='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}',
'Cache-Control': 'public, max-age=86400', # Cache for 24 hours
},
)
except HTTPException:
raise
except Exception as error:
logger.error(f'Failed to download media {file_id}: {error}')
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='Failed to download media',
) from error
finally:
await bot.session.close()