6409b0c023
When a handler swallows a DB error (e.g. ProgrammingError for missing column), the transaction is aborted but the handler returns normally. The auth middleware then tries db.commit() which fails with DBAPIError. Now catches any exception on commit and does rollback, preventing the cascade of "current transaction is aborted" errors through all subsequent middleware layers.
260 lines
13 KiB
Python
260 lines
13 KiB
Python
import asyncio
|
|
from collections.abc import Awaitable, Callable
|
|
from datetime import UTC, datetime
|
|
from typing import Any
|
|
|
|
import structlog
|
|
from aiogram import BaseMiddleware
|
|
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
|
from aiogram.fsm.context import FSMContext
|
|
from aiogram.types import CallbackQuery, Message, TelegramObject, User as TgUser
|
|
from sqlalchemy.exc import InterfaceError, OperationalError
|
|
|
|
from app.config import settings
|
|
from app.database.crud.user import get_user_by_telegram_id
|
|
from app.database.database import AsyncSessionLocal
|
|
from app.services.remnawave_service import RemnaWaveService
|
|
from app.states import RegistrationStates
|
|
from app.utils.check_reg_process import is_registration_process
|
|
from app.utils.validators import sanitize_telegram_name
|
|
|
|
|
|
logger = structlog.get_logger(__name__)
|
|
|
|
|
|
async def _refresh_remnawave_description(remnawave_uuid: str, description: str, telegram_id: int) -> None:
|
|
try:
|
|
remnawave_service = RemnaWaveService()
|
|
async with remnawave_service.get_api_client() as api:
|
|
await api.update_user(uuid=remnawave_uuid, description=description)
|
|
logger.info('✅ [Middleware] Описание пользователя обновлено в RemnaWave', telegram_id=telegram_id)
|
|
except Exception as remnawave_error:
|
|
logger.error(
|
|
'❌ [Middleware] Ошибка обновления RemnaWave для', telegram_id=telegram_id, remnawave_error=remnawave_error
|
|
)
|
|
|
|
|
|
class AuthMiddleware(BaseMiddleware):
|
|
async def __call__(
|
|
self,
|
|
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
|
|
event: TelegramObject,
|
|
data: dict[str, Any],
|
|
) -> Any:
|
|
# Callback с недоступным сообщением (>48ч) — пропускаем к хендлерам,
|
|
# они сами отправят новое сообщение через edit_or_answer_photo
|
|
# if isinstance(event, CallbackQuery) and isinstance(event.message, InaccessibleMessage):
|
|
# pass # Раньше здесь был return None, теперь пропускаем дальше
|
|
|
|
user: TgUser = None
|
|
if isinstance(event, (Message, CallbackQuery)):
|
|
user = event.from_user
|
|
|
|
if not user:
|
|
return await handler(event, data)
|
|
|
|
if user.is_bot:
|
|
return await handler(event, data)
|
|
|
|
async with AsyncSessionLocal() as db:
|
|
try:
|
|
db_user = await get_user_by_telegram_id(db, user.id)
|
|
|
|
if not db_user:
|
|
state: FSMContext = data.get('state')
|
|
current_state = None
|
|
|
|
if state:
|
|
current_state = await state.get_state()
|
|
|
|
is_reg_process = is_registration_process(event, current_state)
|
|
|
|
is_channel_check = isinstance(event, CallbackQuery) and event.data == 'sub_channel_check'
|
|
|
|
is_start_command = isinstance(event, Message) and event.text and event.text.startswith('/start')
|
|
|
|
if is_reg_process or is_channel_check or is_start_command:
|
|
if is_start_command:
|
|
logger.info('🚀 Пропускаем команду /start от пользователя', user_id=user.id)
|
|
elif is_channel_check:
|
|
logger.info(
|
|
'🔍 Пропускаем незарегистрированного пользователя для проверки канала', user_id=user.id
|
|
)
|
|
else:
|
|
logger.info('🔍 Пропускаем пользователя в процессе регистрации', user_id=user.id)
|
|
data['db'] = db
|
|
data['db_user'] = None
|
|
data['is_admin'] = False
|
|
result = await handler(event, data)
|
|
await db.commit()
|
|
return result
|
|
if isinstance(event, Message):
|
|
await event.answer('▶️ Для начала работы необходимо выполнить команду /start')
|
|
elif isinstance(event, CallbackQuery):
|
|
await event.answer('▶️ Необходимо начать с команды /start', show_alert=True)
|
|
logger.info('🚫 Заблокирован незарегистрированный пользователь', user_id=user.id)
|
|
return None
|
|
from app.database.models import UserStatus
|
|
|
|
if db_user.status == UserStatus.BLOCKED.value:
|
|
if isinstance(event, Message):
|
|
await event.answer('🚫 Ваш аккаунт заблокирован администратором.')
|
|
elif isinstance(event, CallbackQuery):
|
|
await event.answer('🚫 Ваш аккаунт заблокирован администратором.', show_alert=True)
|
|
logger.info('🚫 Заблокированный пользователь попытался использовать бота', user_id=user.id)
|
|
return None
|
|
|
|
if db_user.status == UserStatus.DELETED.value:
|
|
state: FSMContext = data.get('state')
|
|
current_state = None
|
|
|
|
if state:
|
|
current_state = await state.get_state()
|
|
|
|
registration_states = [
|
|
RegistrationStates.waiting_for_language.state,
|
|
RegistrationStates.waiting_for_rules_accept.state,
|
|
RegistrationStates.waiting_for_privacy_policy_accept.state,
|
|
RegistrationStates.waiting_for_referral_code.state,
|
|
]
|
|
|
|
is_start_or_registration = (
|
|
(isinstance(event, Message) and event.text and event.text.startswith('/start'))
|
|
or (current_state in registration_states)
|
|
or (
|
|
isinstance(event, CallbackQuery)
|
|
and event.data
|
|
and (
|
|
event.data
|
|
in [
|
|
'rules_accept',
|
|
'rules_decline',
|
|
'privacy_policy_accept',
|
|
'privacy_policy_decline',
|
|
'referral_skip',
|
|
]
|
|
or event.data.startswith('language_select:')
|
|
)
|
|
)
|
|
)
|
|
|
|
if is_start_or_registration:
|
|
logger.info('🔄 Удаленный пользователь начинает повторную регистрацию', user_id=user.id)
|
|
data['db'] = db
|
|
data['db_user'] = None
|
|
data['is_admin'] = False
|
|
result = await handler(event, data)
|
|
await db.commit()
|
|
return result
|
|
if isinstance(event, Message):
|
|
await event.answer(
|
|
'❌ Ваш аккаунт был удален.\n🔄 Для повторной регистрации выполните команду /start'
|
|
)
|
|
elif isinstance(event, CallbackQuery):
|
|
await event.answer(
|
|
'❌ Ваш аккаунт был удален. Для повторной регистрации выполните /start', show_alert=True
|
|
)
|
|
logger.info('❌ Удаленный пользователь попытался использовать бота без /start', user_id=user.id)
|
|
return None
|
|
|
|
profile_updated = False
|
|
|
|
if db_user.username != user.username:
|
|
old_username = db_user.username
|
|
db_user.username = user.username
|
|
logger.info(
|
|
'🔄 [Middleware] Username обновлен для',
|
|
user_id=user.id,
|
|
old_username=old_username,
|
|
username=db_user.username,
|
|
)
|
|
profile_updated = True
|
|
|
|
safe_first = sanitize_telegram_name(user.first_name)
|
|
safe_last = sanitize_telegram_name(user.last_name)
|
|
if db_user.first_name != safe_first:
|
|
old_first_name = db_user.first_name
|
|
db_user.first_name = safe_first
|
|
logger.info(
|
|
'🔄 [Middleware] Имя обновлено для',
|
|
user_id=user.id,
|
|
old_first_name=old_first_name,
|
|
first_name=db_user.first_name,
|
|
)
|
|
profile_updated = True
|
|
|
|
if db_user.last_name != safe_last:
|
|
old_last_name = db_user.last_name
|
|
db_user.last_name = safe_last
|
|
logger.info(
|
|
'🔄 [Middleware] Фамилия обновлена для',
|
|
user_id=user.id,
|
|
old_last_name=old_last_name,
|
|
last_name=db_user.last_name,
|
|
)
|
|
profile_updated = True
|
|
|
|
db_user.last_activity = datetime.now(UTC)
|
|
|
|
if profile_updated:
|
|
db_user.updated_at = datetime.now(UTC)
|
|
logger.info('💾 [Middleware] Профиль пользователя обновлен в middleware', user_id=user.id)
|
|
|
|
if db_user.remnawave_uuid:
|
|
description = settings.format_remnawave_user_description(
|
|
full_name=db_user.full_name, username=db_user.username, telegram_id=db_user.telegram_id
|
|
)
|
|
asyncio.create_task(
|
|
_refresh_remnawave_description(
|
|
remnawave_uuid=db_user.remnawave_uuid,
|
|
description=description,
|
|
telegram_id=db_user.telegram_id,
|
|
)
|
|
)
|
|
|
|
data['db'] = db
|
|
data['db_user'] = db_user
|
|
data['is_admin'] = settings.is_admin(user.id)
|
|
|
|
result = await handler(event, data)
|
|
try:
|
|
await db.commit()
|
|
except (InterfaceError, OperationalError) as conn_err:
|
|
# Соединение закрылось (таймаут после долгой операции) - просто логируем
|
|
logger.warning('⚠️ Соединение с БД закрыто после обработки, пропускаем commit', conn_err=conn_err)
|
|
except Exception as commit_err:
|
|
# Transaction aborted (e.g. handler swallowed a ProgrammingError) — rollback
|
|
logger.warning('⚠️ Не удалось commit после обработки, rollback', commit_err=commit_err)
|
|
try:
|
|
await db.rollback()
|
|
except Exception:
|
|
pass
|
|
return result
|
|
|
|
except (InterfaceError, OperationalError) as conn_err:
|
|
# Соединение с БД закрылось - не пытаемся rollback
|
|
logger.error('Ошибка соединения с БД в AuthMiddleware', conn_err=conn_err)
|
|
logger.error('Event type', event_type=type(event))
|
|
if hasattr(event, 'data'):
|
|
logger.error('Callback data', event_data=event.data)
|
|
raise
|
|
except TelegramForbiddenError:
|
|
# User blocked the bot — normal, not an error
|
|
logger.debug('AuthMiddleware: bot blocked by user, skipping')
|
|
return None
|
|
except TelegramBadRequest as e:
|
|
if 'query is too old' in str(e):
|
|
logger.debug('AuthMiddleware: callback query expired, skipping')
|
|
return None
|
|
raise
|
|
except Exception as e:
|
|
logger.error('Ошибка в AuthMiddleware', error=e)
|
|
logger.error('Event type', event_type=type(event))
|
|
if hasattr(event, 'data'):
|
|
logger.error('Callback data', event_data=event.data)
|
|
try:
|
|
await db.rollback()
|
|
except (InterfaceError, OperationalError):
|
|
pass # Соединение уже закрыто
|
|
raise
|