From c7114ec3593e98824986e3ae3c5dafe6547dadcc Mon Sep 17 00:00:00 2001 From: Egor Date: Fri, 21 Nov 2025 03:24:17 +0300 Subject: [PATCH] Clean up Telegram Stars top-up messages --- app/handlers/balance/stars.py | 81 ++++++++++++++++++++++-------- app/handlers/stars_payments.py | 25 ++++++++- app/services/payment/stars.py | 30 ----------- app/utils/stars_message_cleanup.py | 35 +++++++++++++ 4 files changed, 118 insertions(+), 53 deletions(-) create mode 100644 app/utils/stars_message_cleanup.py diff --git a/app/handlers/balance/stars.py b/app/handlers/balance/stars.py index 0dcf0031..15bf5789 100644 --- a/app/handlers/balance/stars.py +++ b/app/handlers/balance/stars.py @@ -1,7 +1,6 @@ import logging from aiogram import types from aiogram.fsm.context import FSMContext -from sqlalchemy.ext.asyncio import AsyncSession from app.config import settings from app.database.models import User @@ -22,11 +21,11 @@ async def start_stars_payment( state: FSMContext ): texts = get_texts(db_user.language) - + if not settings.TELEGRAM_STARS_ENABLED: await callback.answer("❌ Пополнение через Stars временно недоступно", show_alert=True) return - + # Формируем текст сообщения в зависимости от настройки if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: message_text = ( @@ -35,10 +34,10 @@ async def start_stars_payment( ) else: message_text = texts.TOP_UP_AMOUNT - + # Создаем клавиатуру keyboard = get_back_keyboard(db_user.language) - + # Если включен быстрый выбор суммы и не отключены кнопки, добавляем кнопки if settings.YOOKASSA_QUICK_AMOUNT_SELECTION_ENABLED and not settings.DISABLE_TOPUP_BUTTONS: from .main import get_quick_amount_buttons @@ -46,14 +45,17 @@ async def start_stars_payment( if quick_amount_buttons: # Вставляем кнопки быстрого выбора перед кнопкой "Назад" keyboard.inline_keyboard = quick_amount_buttons + keyboard.inline_keyboard - + await callback.message.edit_text( message_text, reply_markup=keyboard ) - + await state.set_state(BalanceStates.waiting_for_amount) - await state.update_data(payment_method="stars") + await state.update_data( + payment_method="stars", + prompt_message_id=callback.message.message_id, + ) await callback.answer() @@ -65,40 +67,75 @@ async def process_stars_payment_amount( state: FSMContext ): texts = get_texts(db_user.language) - + if not settings.TELEGRAM_STARS_ENABLED: await message.answer("⚠️ Оплата Stars временно недоступна") return - + try: amount_rubles = amount_kopeks / 100 stars_amount = TelegramStarsService.calculate_stars_from_rubles(amount_rubles) - stars_rate = settings.get_stars_rate() - + stars_rate = settings.get_stars_rate() + payment_service = PaymentService(message.bot) invoice_link = await payment_service.create_stars_invoice( amount_kopeks=amount_kopeks, description=f"Пополнение баланса на {texts.format_price(amount_kopeks)}", payload=f"balance_{db_user.id}_{amount_kopeks}" ) - + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ [types.InlineKeyboardButton(text="⭐ Оплатить", url=invoice_link)], [types.InlineKeyboardButton(text=texts.BACK, callback_data="balance_topup")] ]) - - await message.answer( - f"⭐ Оплата через Telegram Stars\n\n" - f"💰 Сумма: {texts.format_price(amount_kopeks)}\n" - f"⭐ К оплате: {stars_amount} звезд\n" - f"📊 Курс: {stars_rate}₽ за звезду\n\n" + + prompt_message_id = (await state.get_data()).get("prompt_message_id") + + if prompt_message_id: + try: + await message.bot.delete_message( + chat_id=message.chat.id, + message_id=prompt_message_id, + ) + except Exception: + logger.warning( + "Не удалось удалить сообщение с вводом суммы Stars", + exc_info=True, + ) + + try: + await message.delete() + except Exception: + logger.warning( + "Не удалось удалить сообщение пользователя с суммой", + exc_info=True, + ) + + invoice_message = await message.answer( + f"⭐ Оплата через Telegram Stars\n\n", + f"💰 Сумма: {texts.format_price(amount_kopeks)}\n", + f"⭐ К оплате: {stars_amount} звезд\n", + f"📊 Курс: {stars_rate}₽ за звезду\n\n", f"Нажмите кнопку ниже для оплаты:", reply_markup=keyboard, parse_mode="HTML" ) - + + try: + from app.utils.stars_message_cleanup import register_stars_payment_messages + + await register_stars_payment_messages( + user_id=message.from_user.id, + message_ids=[invoice_message.message_id], + ) + except Exception: + logger.warning( + "Не удалось зарегистрировать сообщения Stars для очистки", + exc_info=True, + ) + await state.clear() - + except Exception as e: logger.error(f"Ошибка создания Stars invoice: {e}") - await message.answer("⚠️ Ошибка создания платежа") \ No newline at end of file + await message.answer("⚠️ Ошибка создания платежа") diff --git a/app/handlers/stars_payments.py b/app/handlers/stars_payments.py index 4b49f51f..d6e81760 100644 --- a/app/handlers/stars_payments.py +++ b/app/handlers/stars_payments.py @@ -10,6 +10,7 @@ from app.external.telegram_stars import TelegramStarsService from app.database.crud.user import get_user_by_telegram_id from app.localization.loader import DEFAULT_LANGUAGE from app.localization.texts import get_texts +from app.utils.stars_message_cleanup import consume_stars_payment_messages logger = logging.getLogger(__name__) @@ -113,8 +114,30 @@ async def handle_successful_payment( payload=payment.invoice_payload, telegram_payment_charge_id=payment.telegram_payment_charge_id ) - + if success: + cleanup_message_ids = set() + + try: + cleanup_message_ids = await consume_stars_payment_messages(user_id) + except Exception: + logger.warning( + "Не удалось получить сообщения Stars для удаления", + exc_info=True, + ) + + for message_id in cleanup_message_ids: + try: + await message.bot.delete_message( + chat_id=message.chat.id, + message_id=message_id, + ) + except Exception: + logger.warning( + "Не удалось удалить промежуточное сообщение Stars", + exc_info=True, + ) + rubles_amount = TelegramStarsService.calculate_rubles_from_stars(payment.total_amount) amount_kopeks = int((rubles_amount * Decimal(100)).to_integral_value(rounding=ROUND_HALF_UP)) amount_text = settings.format_price(amount_kopeks).replace(" ₽", "") diff --git a/app/services/payment/stars.py b/app/services/payment/stars.py index b7fd5942..3f1022d7 100644 --- a/app/services/payment/stars.py +++ b/app/services/payment/stars.py @@ -515,36 +515,6 @@ class TelegramStarsMixin: exc_info=True, ) - if getattr(self, "bot", None): - try: - keyboard = await self.build_topup_success_keyboard(user) - - charge_id_short = (telegram_payment_charge_id or getattr(transaction, "external_id", ""))[:8] - - await self.bot.send_message( - user.telegram_id, - ( - "✅ Пополнение успешно!\n\n" - f"⭐ Звезд: {stars_amount}\n" - f"💰 Сумма: {settings.format_price(amount_kopeks)}\n" - "🦊 Способ: Telegram Stars\n" - f"🆔 Транзакция: {charge_id_short}...\n\n" - "Баланс пополнен автоматически!" - ), - parse_mode="HTML", - reply_markup=keyboard, - ) - logger.info( - "✅ Отправлено уведомление пользователю %s о пополнении на %s", - user.telegram_id, - settings.format_price(amount_kopeks), - ) - except Exception as error: # pragma: no cover - диагностический лог - logger.error( - "Ошибка отправки уведомления о пополнении Stars: %s", - error, - ) - # Проверяем наличие сохраненной корзины для возврата к оформлению подписки try: from aiogram import types diff --git a/app/utils/stars_message_cleanup.py b/app/utils/stars_message_cleanup.py new file mode 100644 index 00000000..932ed8e5 --- /dev/null +++ b/app/utils/stars_message_cleanup.py @@ -0,0 +1,35 @@ +"""Вспомогательные функции для очистки служебных сообщений Telegram Stars.""" + +from __future__ import annotations + +import asyncio +from typing import Iterable, Set + +__all__ = [ + "register_stars_payment_messages", + "consume_stars_payment_messages", +] + + +_messages_lock = asyncio.Lock() +_messages_to_cleanup: dict[int, set[int]] = {} + + +async def register_stars_payment_messages(user_id: int, message_ids: Iterable[int]) -> None: + """Сохраняет идентификаторы сообщений для последующего удаления. + + Args: + user_id: Telegram ID пользователя. + message_ids: Сообщения, которые нужно удалить после успешной оплаты. + """ + + async with _messages_lock: + existing = _messages_to_cleanup.setdefault(user_id, set()) + existing.update(message_ids) + + +async def consume_stars_payment_messages(user_id: int) -> Set[int]: + """Возвращает и удаляет накопленные сообщения для пользователя.""" + + async with _messages_lock: + return _messages_to_cleanup.pop(user_id, set())