Files
remnawave-bedolaga-telegram…/app/utils/decorators.py
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

185 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import functools
from collections.abc import Callable
from typing import Any
import structlog
from aiogram import types
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
from app.config import settings
from app.localization.texts import get_texts
logger = structlog.get_logger(__name__)
def admin_required(func: Callable) -> Callable:
@functools.wraps(func)
async def wrapper(event: types.Update, *args, **kwargs) -> Any:
user = None
if isinstance(event, (types.Message, types.CallbackQuery)):
user = event.from_user
if not user or not settings.is_admin(user.id):
texts = get_texts()
try:
if isinstance(event, types.Message):
await event.answer(texts.ACCESS_DENIED)
elif isinstance(event, types.CallbackQuery):
await event.answer(texts.ACCESS_DENIED, show_alert=True)
except TelegramBadRequest as e:
if 'query is too old' in str(e).lower():
logger.warning(
'Попытка ответить на устаревший callback query от', user_id=user.id if user else 'Unknown'
)
else:
raise
logger.warning('Попытка доступа к админской функции от', user_id=user.id if user else 'Unknown')
return None
return await func(event, *args, **kwargs)
return wrapper
def auth_required(func: Callable) -> Callable:
"""
Простая проверка на наличие пользователя в апдейте. Middleware уже подтягивает db_user,
но здесь страхуемся от вызовов без from_user.
"""
@functools.wraps(func)
async def wrapper(event: types.Update, *args, **kwargs) -> Any:
user = None
if isinstance(event, (types.Message, types.CallbackQuery)):
user = event.from_user
if not user:
logger.warning('auth_required: нет from_user, пропускаем')
return None
return await func(event, *args, **kwargs)
return wrapper
def error_handler(func: Callable) -> Callable:
@functools.wraps(func)
async def wrapper(*args, **kwargs) -> Any:
try:
return await func(*args, **kwargs)
except TelegramBadRequest as e:
error_message = str(e).lower()
if 'query is too old' in error_message or 'query id is invalid' in error_message:
event = _extract_event(args)
if event and isinstance(event, types.CallbackQuery):
user_info = (
f'@{event.from_user.username}' if event.from_user.username else f'ID:{event.from_user.id}'
)
logger.warning(
'🕐 Игнорируем устаревший callback от в',
event_data=event.data,
user_info=user_info,
__name__=func.__name__,
)
else:
logger.warning('🕐 Игнорируем устаревший запрос в', __name__=func.__name__, error=e)
return None
if 'message is not modified' in error_message:
logger.debug('📝 Сообщение не изменено в', __name__=func.__name__)
event = _extract_event(args)
if event and isinstance(event, types.CallbackQuery):
try:
await event.answer()
except TelegramBadRequest as answer_error:
if 'query is too old' not in str(answer_error).lower():
logger.error(
'Ошибка при ответе на callback в', __name__=func.__name__, answer_error=answer_error
)
return None
logger.error('Telegram API error в', __name__=func.__name__, error=e)
# Уведомление отправляется в _send_error_message
await _send_error_message(args, kwargs, e, func.__name__)
except Exception as e:
logger.error('Ошибка в', __name__=func.__name__, error=e, exc_info=True)
await _send_error_message(args, kwargs, e, func.__name__)
return wrapper
def _extract_event(args) -> types.TelegramObject:
for arg in args:
if isinstance(arg, (types.Message, types.CallbackQuery)):
return arg
return None
async def _send_error_message(args, kwargs, original_error, func_name: str = 'unknown'):
event = _extract_event(args)
db_user = kwargs.get('db_user')
# Отправляем сообщение пользователю
try:
if not event:
return
texts = get_texts(db_user.language if db_user else 'ru')
if isinstance(event, types.Message):
await event.answer(texts.ERROR)
elif isinstance(event, types.CallbackQuery):
await event.answer(texts.ERROR, show_alert=True)
except TelegramBadRequest as e:
if 'query is too old' in str(e).lower():
logger.warning('Не удалось отправить сообщение об ошибке - callback query устарел')
else:
logger.warning('Ошибка при отправке сообщения об ошибке', error=e)
except Exception as e:
logger.warning('Критическая ошибка при отправке сообщения об ошибке', error=e)
def state_cleanup(func: Callable) -> Callable:
@functools.wraps(func)
async def wrapper(*args, **kwargs) -> Any:
state = kwargs.get('state')
try:
return await func(*args, **kwargs)
except Exception as e:
if state and isinstance(state, FSMContext):
await state.clear()
raise e
return wrapper
def typing_action(func: Callable) -> Callable:
@functools.wraps(func)
async def wrapper(event: types.Update, *args, **kwargs) -> Any:
if isinstance(event, types.Message):
try:
await event.bot.send_chat_action(chat_id=event.chat.id, action='typing')
except Exception as e:
logger.warning('Не удалось отправить typing action', error=e)
return await func(event, *args, **kwargs)
return wrapper
def rate_limit(rate: float = 1.0, key: str = None):
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
async def wrapper(event: types.Update, *args, **kwargs) -> Any:
return await func(event, *args, **kwargs)
return wrapper
return decorator