Files
remnawave-bedolaga-telegram…/app/utils/photo_message.py
Fringg a3903a252e refactor: remove smart auto-activation & activation prompt, fix production bugs
Remove AUTO_ACTIVATE_AFTER_TOPUP and SHOW_ACTIVATION_PROMPT_AFTER_TOPUP
features from all payment providers, config, system settings, and tests.
Cart auto-purchase (AUTO_PURCHASE_AFTER_TOPUP) is preserved.

Bug fixes:
- fix KeyError 'months' in devices.py for custom locale overrides
- fix IntegrityError on trial subscription retry (update existing PENDING instead of INSERT)
- fix PendingRollbackError cascade by adding db.rollback() before recovery
- fix TelegramForbiddenError not caught in photo_message.py
- fix "query is too old" spam in required_sub_channel_check
- add missing trial locale keys (TRIAL_PAYMENT_DESCRIPTION, TRIAL_REFUND_DESCRIPTION, TRIAL_ACTIVATION_ERROR)
2026-02-09 21:39:53 +03:00

206 lines
7.7 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 asyncio
import logging
from aiogram import types
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError, TelegramNetworkError
from aiogram.types import InaccessibleMessage, InputMediaPhoto
from app.config import settings
from .message_patch import (
LOGO_PATH,
_cache_logo_file_id,
append_privacy_hint,
get_logo_media,
is_privacy_restricted_error,
is_qr_message,
prepare_privacy_safe_kwargs,
)
logger = logging.getLogger(__name__)
MAX_RETRIES = 3
RETRY_DELAY = 0.5
def _resolve_media(message: types.Message):
if isinstance(message, InaccessibleMessage):
return get_logo_media()
if settings.ENABLE_LOGO_MODE and not is_qr_message(message):
return get_logo_media()
if message.photo:
return message.photo[-1].file_id
return get_logo_media()
def _get_language(callback: types.CallbackQuery) -> str | None:
try:
user = callback.from_user
if user and getattr(user, 'language_code', None):
return user.language_code
except AttributeError:
pass
return None
def _build_base_kwargs(keyboard: types.InlineKeyboardMarkup | None, parse_mode: str | None):
kwargs: dict[str, object] = {}
if parse_mode is not None:
kwargs['parse_mode'] = parse_mode
if keyboard is not None:
kwargs['reply_markup'] = keyboard
return kwargs
async def _answer_text(
callback: types.CallbackQuery,
caption: str,
keyboard: types.InlineKeyboardMarkup | None,
parse_mode: str | None,
error: TelegramBadRequest | None = None,
) -> None:
language = _get_language(callback)
kwargs = _build_base_kwargs(keyboard, parse_mode)
if error and is_privacy_restricted_error(error):
caption = append_privacy_hint(caption, language)
kwargs = prepare_privacy_safe_kwargs(kwargs)
kwargs.setdefault('parse_mode', parse_mode or 'HTML')
await callback.message.answer(
caption,
**kwargs,
)
async def edit_or_answer_photo(
callback: types.CallbackQuery,
caption: str,
keyboard: types.InlineKeyboardMarkup,
parse_mode: str | None = 'HTML',
*,
force_text: bool = False,
) -> None:
resolved_parse_mode = parse_mode or 'HTML'
# Если сообщение недоступно, отправляем новое сообщение
if isinstance(callback.message, InaccessibleMessage):
try:
if settings.ENABLE_LOGO_MODE and LOGO_PATH.exists():
result = await callback.message.answer_photo(
photo=get_logo_media(),
caption=caption,
reply_markup=keyboard,
parse_mode=resolved_parse_mode,
)
_cache_logo_file_id(result)
else:
await callback.message.answer(
caption,
reply_markup=keyboard,
parse_mode=resolved_parse_mode,
)
except Exception as e:
logger.warning('Не удалось отправить новое сообщение для InaccessibleMessage: %s', e)
try:
await callback.message.answer(
caption,
reply_markup=keyboard,
parse_mode=resolved_parse_mode,
)
except Exception:
pass
return
# Если режим логотипа выключен или требуется текстовое сообщение — работаем текстом
if force_text or not settings.ENABLE_LOGO_MODE:
try:
if callback.message.photo:
await callback.message.delete()
await _answer_text(callback, caption, keyboard, resolved_parse_mode)
else:
await callback.message.edit_text(
caption,
reply_markup=keyboard,
parse_mode=resolved_parse_mode,
)
except TelegramForbiddenError:
logger.debug('Пользователь заблокировал бота, пропускаем')
except TelegramBadRequest as error:
try:
await callback.message.delete()
except Exception:
pass
await _answer_text(callback, caption, keyboard, resolved_parse_mode, error)
return
# Если текст слишком длинный для caption — отправим как текст
if caption and len(caption) > 1000:
try:
if callback.message.photo:
await callback.message.delete()
await _answer_text(callback, caption, keyboard, resolved_parse_mode)
except TelegramForbiddenError:
logger.debug('Пользователь заблокировал бота, пропускаем')
except TelegramBadRequest as error:
await _answer_text(callback, caption, keyboard, resolved_parse_mode, error)
return
media = _resolve_media(callback.message)
# Retry logic для сетевых ошибок
for attempt in range(MAX_RETRIES):
try:
await callback.message.edit_media(
InputMediaPhoto(media=media, caption=caption, parse_mode=(parse_mode or 'HTML')),
reply_markup=keyboard,
)
return # Успешно — выходим
except TelegramNetworkError as net_error:
if attempt < MAX_RETRIES - 1:
logger.warning('Сетевая ошибка edit_media (попытка %d/%d): %s', attempt + 1, MAX_RETRIES, net_error)
await asyncio.sleep(RETRY_DELAY * (attempt + 1))
continue
logger.error('Сетевая ошибка edit_media после %d попыток: %s', MAX_RETRIES, net_error)
# После всех попыток — фоллбек на текст
try:
await callback.message.delete()
except Exception:
pass
await _answer_text(callback, caption, keyboard, resolved_parse_mode)
return
except TelegramForbiddenError:
# Пользователь заблокировал бота — молча игнорируем
logger.debug('Пользователь заблокировал бота, пропускаем edit_media')
return
except TelegramBadRequest as error:
if is_privacy_restricted_error(error):
try:
await callback.message.delete()
except Exception:
pass
await _answer_text(callback, caption, keyboard, resolved_parse_mode, error)
return
# Фоллбек: если не удалось обновить фото — отправим текст
try:
await callback.message.delete()
except Exception:
pass
try:
# Отправим как фото с логотипом
result = await callback.message.answer_photo(
photo=get_logo_media(),
caption=caption,
reply_markup=keyboard,
parse_mode=resolved_parse_mode,
)
_cache_logo_file_id(result)
except (TelegramBadRequest, TelegramForbiddenError) as photo_error:
await _answer_text(callback, caption, keyboard, resolved_parse_mode, photo_error)
except Exception:
# Последний фоллбек — обычный текст
await _answer_text(callback, caption, keyboard, resolved_parse_mode)
return