Files
remnawave-bedolaga-telegram…/app/webapi/routes/media.py
T
Fringg 1f0fef114b refactor: complete structlog migration with contextvars, kwargs, and logging hardening
- Add ContextVarsMiddleware for automatic user_id/chat_id/username binding
  via structlog contextvars (aiogram) and http_method/http_path (FastAPI)
- Use bound_contextvars() context manager instead of clear_contextvars()
  to safely restore previous state instead of wiping all context
- Register ContextVarsMiddleware as outermost middleware (before GlobalError)
  so all error logs include user context
- Replace structlog.get_logger() with structlog.get_logger(__name__) across
  270 calls in 265 files for meaningful logger names
- Switch wrapper_class from BoundLogger to make_filtering_bound_logger()
  for pre-processor level filtering (performance optimization)
- Migrate 1411 %-style positional arg logger calls to structlog kwargs
  style across 161 files via AST script
- Migrate log_rotation_service.py from stdlib logging to structlog
- Add payment module prefixes to TelegramNotifierProcessor.IGNORED_LOGGER_PREFIXES
  and ExcludePaymentFilter.PAYMENT_MODULES to prevent payment data leaking
  to Telegram notifications and general log files
- Fix LoggingMiddleware: add from_user null-safety for channel posts,
  switch time.time() to time.monotonic() for duration measurement
- Remove duplicate logger assignments in purchase.py, config.py,
  inline.py, and admin/payments.py
2026-02-16 09:18:12 +03:00

158 lines
5.0 KiB
Python

from __future__ import annotations
import mimetypes
from typing import Any
import structlog
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 = structlog.get_logger(__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', error=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', file_id=file_id, error=error)
raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Failed to download media') from error
finally:
await bot.session.close()