Files
remnawave-bedolaga-telegram…/app/utils/photo_message.py
T
2026-01-17 02:36:33 +03:00

200 lines
7.5 KiB
Python

import asyncio
import logging
from aiogram import types
from aiogram.exceptions import TelegramBadRequest, TelegramNetworkError
from aiogram.types import FSInputFile, InaccessibleMessage, InputMediaPhoto
from app.config import settings
from .message_patch import (
LOGO_PATH,
append_privacy_hint,
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 FSInputFile(LOGO_PATH)
# Всегда используем логотип если включен режим логотипа,
# кроме специальных случаев (QR сообщения)
if settings.ENABLE_LOGO_MODE and not is_qr_message(message):
return FSInputFile(LOGO_PATH)
# Только если режим логотипа выключен, используем фото из сообщения
elif message.photo:
return message.photo[-1].file_id
return FSInputFile(LOGO_PATH)
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():
await callback.message.answer_photo(
photo=FSInputFile(LOGO_PATH),
caption=caption,
reply_markup=keyboard,
parse_mode=resolved_parse_mode,
)
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 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 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
else:
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 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:
# Отправим как фото с логотипом
await callback.message.answer_photo(
photo=media if isinstance(media, FSInputFile) else FSInputFile(LOGO_PATH),
caption=caption,
reply_markup=keyboard,
parse_mode=resolved_parse_mode,
)
except TelegramBadRequest 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