# - *- coding: utf- 8 - *- import math from typing import Optional, Tuple, Union from aiogram import Bot from aiogram.types import CallbackQuery, LabeledPrice, Message, PreCheckoutQuery, SuccessfulPayment from tgbot.database import Paymentsx from tgbot.utils.const_functions import ded, gen_id, to_number from tgbot.utils.misc.bot_logging import bot_logger from tgbot.utils.misc.bot_models import ARS from tgbot.utils.misc_functions import send_admins STARS_CURRENCY = "XTR" STARS_PAYLOAD_PREFIX = "stars_refill" # АПИ для работы с Telegram Stars class StarsAPI: # Настройка клиента Telegram Stars def __init__( self, bot: Bot, arSession: ARS, update: Optional[Union[Message, CallbackQuery, PreCheckoutQuery]] = None, stars_course: float = 1.5, skipping_error: bool = False, ): self.bot = bot self.arSession = arSession self.update = update self.stars_course = float(stars_course or 1.5) self.skipping_error = skipping_error # Инициализация данных @classmethod async def connect( cls, bot: Bot, arSession: ARS, update: Optional[Union[Message, CallbackQuery, PreCheckoutQuery]] = None, skipping_error: bool = False, ) -> "StarsAPI": payments = await Paymentsx().get() return cls( bot=bot, arSession=arSession, update=update, stars_course=payments.stars_course, skipping_error=skipping_error, ) # Уведомление админов о проблеме со Stars async def error_notification(self, error_code: str = "Unknown") -> None: bot_logger.warning("Telegram Stars недоступны: %s", error_code) if self.skipping_error: return await send_admins( self.bot, f"⭐️ Telegram Stars недоступны\n" f"❗️ Ошибка: {error_code}" ) # Проверка доступности Stars для бота async def check(self) -> Tuple[bool, str]: try: bot_info = await self.bot.get_me() balance = await self.bot.get_my_star_balance() except Exception as ex: bot_logger.warning("Не удалось проверить Telegram Stars", exc_info=True) await self.error_notification(str(ex)) return False, "⭐️ Не удалось проверить Telegram Stars ❌" rub_balance = self.stars_to_rub(balance.amount) return True, ded(f""" ⭐️ Telegram Stars полностью функционируют ✅ ➖➖➖➖➖➖➖➖➖➖ ▪️ Бот: @{bot_info.username} ▪️ Баланс: {balance.amount} ⭐️{rub_balance}₽ ▪️ Курс: 1 ⭐️ = {self.stars_course}₽ """) # Получение баланса Stars у бота async def balance(self) -> str: try: balance = await self.bot.get_my_star_balance() except Exception as ex: bot_logger.warning("Не удалось получить баланс Telegram Stars", exc_info=True) await self.error_notification(str(ex)) return "⭐️ Не удалось получить баланс Telegram Stars ❌" rub_balance = self.stars_to_rub(balance.amount) nanostars = balance.nanostar_amount or 0 return ded(f""" ⭐️ Баланс Telegram Stars ➖➖➖➖➖➖➖➖➖➖ ▪️ Звёзды: {balance.amount} ⭐️ ▪️ Нанозвёзды: {nanostars} ▪️ В рублях по курсу: {rub_balance}₽ ▪️ Курс: 1 ⭐️ = {self.stars_course}₽ """) # Создание ссылки на счёт Telegram Stars async def bill(self, pay_amount: Union[float, int]) -> Tuple[Union[str, bool], str, int]: rub_amount = round(float(pay_amount), 2) stars_amount = self.rub_to_stars(rub_amount) bill_chat_id, bill_message_id = self.get_bill_message() receipt = gen_id(12) payload = self.make_payload( receipt, rub_amount, stars_amount, bill_chat_id, bill_message_id, ) try: bill_link = await self.bot.create_invoice_link( title="Пополнение баланса", description=f"Пополнение баланса на {rub_amount}₽ через Telegram Stars", payload=payload, provider_token="", currency=STARS_CURRENCY, prices=[ LabeledPrice( label=f"Пополнение на {rub_amount}₽", amount=stars_amount, ) ], ) except Exception as ex: bot_logger.warning("Не удалось создать счёт Telegram Stars", exc_info=True) await self.error_notification(str(ex)) return False, "", 0 bill_message = ded(f""" 💰 Пополнение баланса через Telegram Stars ➖➖➖➖➖➖➖➖➖➖ ▪️ Сумма пополнения: {rub_amount}₽ ▪️ К оплате: {stars_amount} ⭐️ ▪️ Курс: 1 ⭐️ = {self.stars_course}₽ ➖➖➖➖➖➖➖➖➖➖ ❗️ После оплаты средства автоматически зачислятся на ваш баланс Если сообщение не обновилось, нажмите Проверить оплату """) return bill_message, bill_link, receipt # Проверка оплаты по истории Stars async def bill_check(self, bill_receipt: Optional[Union[str, int]] = None, records: int = 100) -> Tuple[int, float]: if bill_receipt is None: return 1, 0 try: transactions = await self.bot.get_star_transactions(limit=records) except Exception as ex: bot_logger.warning("Не удалось проверить оплату Telegram Stars", exc_info=True) await self.error_notification(str(ex)) return 1, 0 receipt = str(bill_receipt) for transaction in transactions.transactions: source = transaction.source payload = getattr(source, "invoice_payload", None) if not payload: continue payload_data = self.parse_payload(payload) if payload_data is None or str(payload_data[0]) != receipt: continue _receipt, rub_amount, stars_amount, _bill_chat_id, _bill_message_id = payload_data if transaction.amount != stars_amount: bot_logger.warning( "Сумма Telegram Stars не совпала с payload: receipt=%s expected=%s actual=%s", receipt, stars_amount, transaction.amount, ) return 1, 0 return 0, rub_amount return 2, 0 # Подтверждение автоматической оплаты async def answer_pre_checkout(self, query: PreCheckoutQuery) -> None: payload_data = self.parse_payload(query.invoice_payload) if query.currency != STARS_CURRENCY or payload_data is None or query.total_amount != payload_data[2]: await self.bot.answer_pre_checkout_query( pre_checkout_query_id=query.id, ok=False, error_message="Некорректный счёт Telegram Stars", ) return await self.bot.answer_pre_checkout_query( pre_checkout_query_id=query.id, ok=True, ) # Получение данных успешной оплаты def parse_successful_payment( self, payment: SuccessfulPayment, ) -> Tuple[int, float, str, Optional[int], Optional[int]]: payload_data = self.parse_payload(payment.invoice_payload) if payment.currency != STARS_CURRENCY or payload_data is None or payment.total_amount != payload_data[2]: raise ValueError("Некорректная успешная оплата Telegram Stars") receipt, rub_amount, _stars_amount, bill_chat_id, bill_message_id = payload_data return receipt, rub_amount, payment.telegram_payment_charge_id, bill_chat_id, bill_message_id # Возврат оплаты пользователю async def refund(self, user_id: int, telegram_payment_charge_id: str) -> bool: try: return await self.bot.refund_star_payment( user_id=user_id, telegram_payment_charge_id=telegram_payment_charge_id, ) except Exception as ex: bot_logger.warning("Не удалось вернуть оплату Telegram Stars", exc_info=True) await self.error_notification(str(ex)) return False # Конвертация рублей в звёзды def rub_to_stars(self, rub_amount: Union[float, int]) -> int: if self.stars_course <= 0: raise ValueError("Курс Telegram Stars должен быть больше нуля") return max(1, math.ceil(float(rub_amount) / self.stars_course)) # Конвертация звёзд в рубли def stars_to_rub(self, stars_amount: Union[float, int]) -> float: return round(float(stars_amount) * self.stars_course, 2) # Получение данных сообщения со счётом def get_bill_message(self) -> Tuple[Optional[int], Optional[int]]: if isinstance(self.update, Message): return self.update.chat.id, self.update.message_id return None, None # Сбор payload для телеграм инвойса @staticmethod def make_payload( receipt: int, rub_amount: float, stars_amount: int, bill_chat_id: Optional[int] = None, bill_message_id: Optional[int] = None, ) -> str: payload = f"{STARS_PAYLOAD_PREFIX}:{receipt}:{rub_amount}:{stars_amount}" if bill_chat_id is not None and bill_message_id is not None: payload += f":{bill_chat_id}:{bill_message_id}" return payload # Разбор payload телеграм инвойса @staticmethod def parse_payload( payload: str, ) -> Optional[Tuple[int, float, int, Optional[int], Optional[int]]]: payload_parts = str(payload).split(":") if len(payload_parts) not in (4, 6) or payload_parts[0] != STARS_PAYLOAD_PREFIX: return None try: receipt = int(payload_parts[1]) rub_amount = float(to_number(payload_parts[2])) stars_amount = int(payload_parts[3]) bill_chat_id = int(payload_parts[4]) if len(payload_parts) == 6 else None bill_message_id = int(payload_parts[5]) if len(payload_parts) == 6 else None except (TypeError, ValueError): return None return receipt, rub_amount, stars_amount, bill_chat_id, bill_message_id