Files
AutoShop-Djimbo/tgbot/services/api_stars.py
T
2026-07-15 07:17:25 +05:00

298 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# - *- 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"<b>⭐️ Telegram Stars недоступны</b>\n"
f"❗️ Ошибка: <code>{error_code}</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, "<b>⭐️ Не удалось проверить Telegram Stars ❌</b>"
rub_balance = self.stars_to_rub(balance.amount)
return True, ded(f"""
<b>⭐️ Telegram Stars полностью функционируют ✅</b>
➖➖➖➖➖➖➖➖➖➖
▪️ Бот: <code>@{bot_info.username}</code>
▪️ Баланс: <code>{balance.amount} ⭐️</code> ≈ <code>{rub_balance}₽</code>
▪️ Курс: <code>1 ⭐️ = {self.stars_course}₽</code>
""")
# Получение баланса 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 "<b>⭐️ Не удалось получить баланс Telegram Stars ❌</b>"
rub_balance = self.stars_to_rub(balance.amount)
nanostars = balance.nanostar_amount or 0
return ded(f"""
<b>⭐️ Баланс Telegram Stars</b>
➖➖➖➖➖➖➖➖➖➖
▪️ Звёзды: <code>{balance.amount} ⭐️</code>
▪️ Нанозвёзды: <code>{nanostars}</code>
▪️ В рублях по курсу: <code>{rub_balance}₽</code>
▪️ Курс: <code>1 ⭐️ = {self.stars_course}₽</code>
""")
# Создание ссылки на счёт 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"""
<b>💰 Пополнение баланса через Telegram Stars</b>
➖➖➖➖➖➖➖➖➖➖
▪️ Сумма пополнения: <code>{rub_amount}₽</code>
▪️ К оплате: <code>{stars_amount} ⭐️</code>
▪️ Курс: <code>1 ⭐️ = {self.stars_course}₽</code>
➖➖➖➖➖➖➖➖➖➖
❗️ После оплаты средства автоматически зачислятся на ваш баланс
Если сообщение не обновилось, нажмите <code>Проверить оплату</code>
""")
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