# - *- 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