mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-23 21:01:17 +00:00
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)
206 lines
7.7 KiB
Python
206 lines
7.7 KiB
Python
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
|