Initial local state

This commit is contained in:
2026-07-15 07:17:25 +05:00
commit 68221821a5
78 changed files with 10318 additions and 0 deletions
View File
+219
View File
@@ -0,0 +1,219 @@
# - *- coding: utf- 8 - *-
import json
from typing import Any, Dict, Optional, Tuple, Union
from aiogram import Bot
from aiogram.types import CallbackQuery, Message
from aiohttp import ClientConnectorCertificateError
from tgbot.database import Paymentsx
from tgbot.utils.const_functions import ded, 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
# Список поддерживаемых валют
ALLOW_CURRENCIES = ['BTC', 'ETH', 'LTC', 'USDT', 'USDC', 'TRX', 'TON', 'BNB', 'SOL', 'DOGE']
# АПИ для работы с CryptoBot
class CryptobotAPI:
# Настройка клиента CryptoBot
def __init__(
self,
bot: Bot,
arSession: ARS,
update: Optional[Union[Message, CallbackQuery]] = None,
token: str = "None",
adding: bool = False,
skipping_error: bool = False,
):
self.bot = bot
self.arSession = arSession
self.update = update
self.token = token
self.adding = adding
self.skipping_error = skipping_error
# Инициализация данных
@classmethod
async def connect(
cls,
bot: Bot,
arSession: ARS,
update: Optional[Union[Message, CallbackQuery]] = None,
token: Optional[str] = None,
skipping_error: bool = False,
) -> "CryptobotAPI":
adding = token is not None
if token is None:
payments = await Paymentsx().get()
token = payments.cryptobot_token
adding = False
return cls(
bot=bot,
arSession=arSession,
update=update,
token=token,
adding=adding,
skipping_error=skipping_error,
)
# Уведомления о неработоспособности кассы/кошелька
async def error_notification(self, error_code: str = "Unknown"):
bot_logger.warning("CryptoBot недоступен: %s", error_code)
if not self.skipping_error:
if self.adding and self.update is not None:
await self.update.edit_text(
f"<b>🔷 Не удалось добавить CryptoBot кассу ❌</b>\n"
f"❗️ Ошибка: <code>{error_code}</code>"
)
else:
await send_admins(
self.bot,
f"<b>🔷 CryptoBot недоступен. Как можно быстрее его замените</b>\n"
f"❗️ Ошибка: <code>{error_code}</code>"
)
# Проверка кассы/кошелька
async def check(self) -> Tuple[bool, str]:
status, response = await self._request("getMe")
if status and response['ok']:
return True, ded(f"""
<b>🔷 CryptoBot кошелёк полностью функционирует ✅</b>
➖➖➖➖➖➖➖➖➖➖
▪️ Токен: <code>{self.token}</code>
▪️ Айди: <code>{response['result']['app_id']}</code>
▪️ Имя: <code>{response['result']['name']}</code>
""")
return False, "<b>🔷 Не удалось проверить CryptoBot кошелёк ❌</b>"
# Получение баланса
async def balance(self) -> str:
status, response = await self._request("getBalance")
if status and response['ok']:
save_currencies = []
response_balances = sorted(
response['result'],
reverse=True,
key=lambda balance: to_number(balance['available']),
)
for currency in response_balances:
if currency['currency_code'] in ALLOW_CURRENCIES:
save_currencies.append(
f"▪️ {currency['currency_code']}: <code>{currency['available']}</code>"
)
save_currencies = "\n".join(save_currencies)
return ded(f"""
<b>🔷 Баланс CryptoBot кошелька составляет</b>
➖➖➖➖➖➖➖➖➖➖
{save_currencies}
""")
return "<b>🔷 Не удалось получить баланс CryptoBot кошелька ❌</b>"
# Создание счета на оплату
async def bill(self, pay_amount: Union[float, int]) -> Tuple[Union[str, bool], str, str]:
assets_currencies = ",".join(ALLOW_CURRENCIES)
payload = {
'currency_type': 'fiat',
'fiat': 'RUB',
'amount': str(pay_amount),
'expires_in': 10800,
'accepted_assets': assets_currencies
}
status, response = await self._request("createInvoice", payload)
if status and response['ok']:
bill_message = ded(f"""
<b>💰 Пополнение баланса</b>
➖➖➖➖➖➖➖➖➖➖
▪️ Для пополнения баланса, нажмите на кнопку ниже
<code>Перейти к оплате</code> и оплатите выставленный вам счёт
▪️ У вас имеется 3 часа на оплату счета
▪️ Сумма пополнения: <code>{pay_amount}₽</code>
➖➖➖➖➖➖➖➖➖➖
❗️ После оплаты, нажмите на <code>Проверить оплату</code>
""")
return bill_message, response['result']['mini_app_invoice_url'], response['result']['invoice_id']
return False, "", ""
# Проверка счета на оплату
async def bill_check(self, bill_receipt: Optional[Union[str, int]] = None, records: int = 1) -> Tuple[int, float]:
payload = {
'invoice_ids': f'{bill_receipt}',
'fiat': 'RUB',
}
status, response = await self._request("getInvoices", payload)
pay_status, pay_amount = 1, 0
if status and response['ok']:
get_invoice = response['result']['items'][0]
if get_invoice['status'] == "active":
pay_status = 2
elif get_invoice['status'] == "expired":
pay_status = 3
else:
pay_status = 0
pay_amount = to_number(get_invoice['amount'])
return pay_status, pay_amount
# Генерация запроса
async def _request(
self,
method: str,
data: Optional[Dict[str, Any]] = None,
) -> Tuple[bool, Any]:
session = await self.arSession.get_session()
base_url = 'https://pay.crypt.bot/api/'
headers = {
'Crypto-Pay-API-Token': self.token,
'Content-Type': 'application/x-www-form-urlencoded',
}
url = base_url + method
try:
response = await session.post(
url=url,
headers=headers,
data=data,
)
response_data = json.loads((await response.read()).decode())
if response.status == 200:
return True, response_data
else:
await self.error_notification(f"{response.status} - {str(response_data)}")
return False, response_data
except ClientConnectorCertificateError:
bot_logger.warning("Ошибка SSL при запросе CryptoBot", exc_info=True)
await self.error_notification("CERTIFICATE_VERIFY_FAILED")
return False, "CERTIFICATE_VERIFY_FAILED"
except Exception as ex:
bot_logger.warning("Ошибка запроса CryptoBot", exc_info=True)
await self.error_notification(str(ex))
return False, str(ex)
+239
View File
@@ -0,0 +1,239 @@
# - *- coding: utf- 8 - *-
import asyncio
import json
from io import BytesIO
from typing import Any, Dict, Optional, Tuple, Union
import ujson
from aiogram import Bot
from aiogram.types import CallbackQuery, Message
from aiohttp import ClientConnectorCertificateError, FormData
from tgbot.database import Settingsx
from tgbot.utils.const_functions import gen_id, send_errors
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
class DiscordAPI:
# Настройка клиента Discord webhook
def __init__(
self,
bot: Bot,
arSession: ARS,
update: Optional[Union[Message, CallbackQuery]] = None,
webhook_url: str = "None",
adding: bool = False,
skipping_error: bool = False,
):
self.bot = bot
self.arSession = arSession
self.update = update
self.adding = adding
self.skipping_error = skipping_error
self.webhook_username = "Djimbo Shop | Free Bot"
self.base_url = "https://djimbo.dev/dsapi"
webhook_url = self._normalize_webhook_url(webhook_url)
if "/" in webhook_url:
self.webhook_id = webhook_url.split("/")[0]
self.webhook_token = webhook_url.split("/")[1]
else:
self.webhook_id = ""
self.webhook_token = ""
# Нормализация Discord webhook URL
@staticmethod
def _normalize_webhook_url(webhook_url: str) -> str:
if webhook_url is None:
return "None"
if webhook_url.startswith("https://discord.com/api/"):
webhook_url = webhook_url[33:]
if webhook_url.startswith("discord.com/api/webhooks/"):
webhook_url = webhook_url[25:]
return webhook_url
# Инициализация данных
@classmethod
async def connect(
cls,
bot: Bot,
arSession: ARS,
update: Optional[Union[Message, CallbackQuery]] = None,
webhook_url: Optional[str] = None,
skipping_error: bool = False,
) -> "DiscordAPI":
adding = webhook_url is not None
if webhook_url is None:
settings = await Settingsx().get()
webhook_url = settings.misc_discord_webhook_url
adding = False
return cls(
bot=bot,
arSession=arSession,
update=update,
webhook_url=webhook_url,
adding=adding,
skipping_error=skipping_error,
)
# Рассылка админам о нерабочем вебхуке
async def error_account_admin(self, error_code: str = "Unknown"):
bot_logger.warning("Discord webhook недоступен: %s", error_code)
if not self.skipping_error:
await send_admins(
self.bot,
f"<b>🧿 Дискорд вебхук недоступен. Как можно быстрее его замените</b>\n"
f"❗️ Ошибка: <code>{error_code}</code>"
)
# Проверка вебхука
async def check(self) -> Tuple[bool, str]:
request_url = f"{self.base_url}/webhooks/{self.webhook_id}/{self.webhook_token}"
status, response = await self._request(
request_url=request_url,
request_method="GET",
)
if status and "channel_id" in response:
discord_channel_id = response['channel_id']
discord_hook_name = response['name']
return True, discord_hook_name
return False, ""
# Загрузка фотографий
async def upload_photo(self, photo_data: Union[BytesIO, bytes], photo_name: Optional[str] = None) -> Tuple[
bool, str]:
request_url = f"{self.base_url}/webhooks/{self.webhook_id}/{self.webhook_token}"
if photo_name is None:
photo_name = str(gen_id(24))
if not photo_name.endswith(".png") and not photo_name.endswith(".jpg"):
photo_name = f"{photo_name}.png"
send_json = {
'username': self.webhook_username,
'content': '',
}
data = FormData()
data.add_field('file', photo_data, filename=photo_name)
data.add_field('payload_json', ujson.dumps(send_json))
await asyncio.sleep(1)
status, response = await self._request(
request_url=request_url,
request_method="POST",
request_data=data,
)
if "id" in response:
channel_id = response['channel_id']
message_id = response['id']
get_discord_forevercdn = await (
DiscordDJ(
arSession=self.arSession,
bot=self.bot,
)
).export_forevercdn()
photo_url = f"{get_discord_forevercdn}/attachments/{channel_id}/{message_id}"
return True, photo_url
return False, "None"
# Запрос
async def _request(
self,
request_url: str,
request_method: str,
request_data: Optional[Union[Dict[str, Any], FormData]] = None,
) -> Tuple[bool, Any]:
session = await self.arSession.get_session()
await asyncio.sleep(1)
try:
response = await session.request(
method=request_method,
url=request_url,
data=request_data,
headers={"Accept-Encoding": "gzip, deflate"},
)
response_data = json.loads((await response.read()).decode())
if response.status == 200:
return True, response_data
else:
await self.error_account_admin(f"{response.status} - {str(response_data)}")
return False, response_data
except ClientConnectorCertificateError:
bot_logger.warning("Ошибка SSL при запросе Discord webhook", exc_info=True)
await self.error_account_admin("CERTIFICATE_VERIFY_FAILED")
return False, "CERTIFICATE_VERIFY_FAILED"
except Exception as ex:
bot_logger.warning("Ошибка запроса Discord webhook", exc_info=True)
await self.error_account_admin(str(ex))
return False, str(ex)
# Извлечение ссылок для работы с локальным АПИ дискорда
class DiscordDJ:
# Настройка клиента сервисных ссылок Discord
def __init__(self, arSession: ARS, bot: Bot):
self.arSession = arSession
self.bot = bot
self.const_url = "https://djimbo.dev/autoshop_discord.json"
# Экспорт публичного дискорд вебхука
async def export_webhook(self) -> str:
session = await self.arSession.get_session()
try:
response = await session.get(
self.const_url,
headers={"Accept-Encoding": "gzip, deflate"},
)
response_data = json.loads((await response.read()).decode())
except Exception as ex:
await send_errors(self.bot, 7729051, f"Error getting Discord Webhook - {ex}")
return "None"
else:
return response_data['webhook']
# Экспорт ссылки на постоянный CDN
async def export_forevercdn(self) -> str:
session = await self.arSession.get_session()
try:
response = await session.get(
self.const_url,
headers={"Accept-Encoding": "gzip, deflate"},
)
response_data = json.loads((await response.read()).decode())
except Exception as ex:
await send_errors(self.bot, 7729051, f"Error getting Discord ForeverCDN - {ex}")
return "None"
else:
return response_data['forevercdn']
+157
View File
@@ -0,0 +1,157 @@
# - *- coding: utf- 8 - *-
import asyncio
import json
from dataclasses import dataclass
from aiogram import Bot
from tgbot.database import Settingsx
from tgbot.services.api_telegraph import TelegraphAPI
from tgbot.utils.const_functions import send_errors
from tgbot.utils.misc.bot_logging import bot_logger
from tgbot.utils.misc.bot_models import ARS
# Model Hosting Text
@dataclass
class ModelHostingText:
telegraph: str = "telegraph"
pastie: str = "pastie"
friendpaste: str = "friendpaste"
snippet: str = "snippet"
# API для работы с текстовыми хостингами
class HostingAPI:
# Настройка клиента текстового хостинга
def __init__(
self,
arSession: ARS,
bot: Bot,
text_hosting: str,
):
self.arSession = arSession
self.bot = bot
self.text_hosting = text_hosting
# Инициализация данных
@classmethod
async def connect(cls, arSession: ARS, bot: Bot):
settings = await Settingsx().get()
return cls(
arSession=arSession,
bot=bot,
text_hosting=settings.misc_hosting_text, # telegraph, pastie, friendpaste, snippet
)
# Загрузка текста на хостинг
async def upload_text(self, text: str) -> str:
session = await self.arSession.get_session()
max_attempts = 10
for attempt in range(max_attempts):
status_success = True
return_link = "None"
############################################################
######################## TELEGRAPH #########################
if self.text_hosting == ModelHostingText.telegraph:
telegraph_api = await TelegraphAPI.connect(arSession=self.arSession)
status, response = await telegraph_api.upload_text(text)
if status:
return_link = response
else:
await send_errors(self.bot, 4044321, str(response))
status_success = False
############################################################
########################## PASTIE ##########################
elif self.text_hosting == ModelHostingText.pastie:
try:
response = await session.request(
method="post",
url="http://pastie.org/pastes/create/",
data={
'language': 'plaintext',
'content': text,
},
)
except Exception as ex:
bot_logger.warning("Ошибка загрузки текста в Pastie", exc_info=True)
await send_errors(self.bot, 4719012, str(ex))
status_success = False
else:
cache_link = response.url
if "create" in str(cache_link) or str(cache_link) == "http://pastie.org/":
status_success = False
else:
return_link = cache_link
############################################################
######################## FRIENDPASTE #######################
elif self.text_hosting == ModelHostingText.friendpaste:
try:
response = await session.request(
method="post",
url="https://www.friendpaste.com/",
json={
'language': 'text',
'title': '',
'snippet': text,
},
)
cache_link = json.loads((await response.read()).decode())['url']
except Exception as ex:
bot_logger.warning("Ошибка загрузки текста в FriendPaste", exc_info=True)
await send_errors(self.bot, 8434711, str(ex))
status_success = False
else:
return_link = cache_link
############################################################
######################### SNIPPET ##########################
elif self.text_hosting == ModelHostingText.snippet:
try:
response = await session.request(
method="post",
url="https://snippet.host/",
data={
'title': '',
'content': text,
'visibility': '1',
'expires': 'never',
'language': 'plain text'
},
)
cache_link = response.url
except Exception as ex:
bot_logger.warning("Ошибка загрузки текста в Snippet", exc_info=True)
await send_errors(self.bot, 8900012, str(ex))
status_success = False
else:
return_link = cache_link
if status_success:
return str(return_link)
# pastie -> friendpaste
if self.text_hosting == ModelHostingText.pastie:
self.text_hosting = ModelHostingText.friendpaste
# friendpaste -> snippet
elif self.text_hosting == ModelHostingText.friendpaste:
self.text_hosting = ModelHostingText.snippet
# snippet -> telegraph
elif self.text_hosting == ModelHostingText.snippet:
self.text_hosting = ModelHostingText.telegraph
# telegraph -> pastie
elif self.text_hosting == ModelHostingText.telegraph:
self.text_hosting = ModelHostingText.pastie
if attempt < max_attempts - 1:
await asyncio.sleep(3)
return "None"
+31
View File
@@ -0,0 +1,31 @@
# - *- coding: utf- 8 - *-
import ssl
from typing import Optional
import aiohttp
import certifi
# Асинхронная сессия для запросов
class AsyncRequestSession:
# Создание ленивой aiohttp-сессии
def __init__(self) -> None:
self._session: Optional[aiohttp.ClientSession] = None
# Получение объекта сессии
async def get_session(self) -> aiohttp.ClientSession:
if self._session is None:
ssl_context = ssl.create_default_context(cafile=certifi.where())
timeout = aiohttp.ClientTimeout(total=15, connect=5, sock_read=10)
connector = aiohttp.TCPConnector(ssl=ssl_context)
self._session = aiohttp.ClientSession(timeout=timeout, connector=connector)
return self._session
# Закрытие сессии
async def close(self) -> None:
if self._session is None:
return None
await self._session.close()
+297
View File
@@ -0,0 +1,297 @@
# - *- 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
+87
View File
@@ -0,0 +1,87 @@
# - *- coding: utf- 8 - *-
import json
from typing import Optional, Tuple
from tgbot.database import Settingsx
from tgbot.utils.const_functions import gen_id
from tgbot.utils.misc.bot_logging import bot_logger
from tgbot.utils.misc.bot_models import ARS
# API для работы с телеграфом
class TelegraphAPI:
# Настройка клиента Telegraph
def __init__(self, arSession: ARS, token: Optional[str] = None):
self.arSession = arSession
self.token = token
self.domain = "https://api.telegra.ph"
# Инициализация данных
@classmethod
async def connect(cls, arSession: ARS, token: Optional[str] = None):
if token is None:
settings = await Settingsx().get()
token = settings.misc_token_telegraph
return cls(arSession=arSession, token=token)
# Загрузка текста в телеграф
async def upload_text(self, text: str) -> Tuple[bool, str]:
session = await self.arSession.get_session()
if self.token == "None" or self.token is None:
status, response = await self.generate_token()
if status:
self.token = response
else:
return False, "Error generate token"
try:
response = await session.request(
method="POST",
url=f"{self.domain}/createPage",
data={
'access_token': self.token,
'title': f'shop{gen_id(18)}',
'content': json.dumps([{'tag': 'p', 'children': [text]}]),
}
)
response_data = json.loads((await response.read()).decode())
except Exception:
bot_logger.warning("Не удалось загрузить текст в Telegraph", exc_info=True)
return False, "None"
else:
if "ok" in response_data and response_data['ok']:
return True, response_data['result']['url'].replace("\\\\", "")
else:
return False, response_data
# Генерация нового Telegraph-токена
async def generate_token(self) -> Tuple[bool, str]:
session = await self.arSession.get_session()
try:
response = await session.request(
method="POST",
url=f"{self.domain}/createAccount",
data={
'short_name': 'dj shop',
}
)
response_data = json.loads((await response.read()).decode())
except Exception:
bot_logger.warning("Не удалось создать Telegraph token", exc_info=True)
return False, "None"
else:
if "ok" in response_data and response_data['ok']:
await Settingsx().update(
misc_token_telegraph=response_data['result']['access_token']
)
return True, response_data['result']['access_token']
else:
return False, response_data
+339
View File
@@ -0,0 +1,339 @@
# - *- coding: utf- 8 - *-
import json
from typing import Any, Dict, Optional, Tuple, Union
from urllib.parse import urlencode
from aiogram import Bot
from aiogram.types import CallbackQuery, Message
from aiohttp import ClientConnectorCertificateError
from tgbot.data.config import YOOMONEY_CLIENT_ID
from tgbot.database import Paymentsx
from tgbot.utils.const_functions import ded, gen_id
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
# АПИ для работы с ЮMoney
class YoomoneyAPI:
# Настройка клиента ЮMoney
def __init__(
self,
bot: Bot,
arSession: ARS,
update: Optional[Union[Message, CallbackQuery]] = None,
token: str = "None",
adding: bool = False,
skipping_error: bool = False,
):
self.token = token
self.adding = adding
self.base_url = 'https://yoomoney.ru/api/'
self.headers = {
'Authorization': f'Bearer {self.token}',
'Content-Type': 'application/x-www-form-urlencoded',
}
self.bot = bot
self.arSession = arSession
self.update = update
self.skipping_error = skipping_error
# Инициализация данных
@classmethod
async def connect(
cls,
bot: Bot,
arSession: ARS,
update: Optional[Union[Message, CallbackQuery]] = None,
token: Optional[str] = None,
skipping_error: bool = False,
) -> "YoomoneyAPI":
adding = token is not None
if token is None:
payments = await Paymentsx().get()
token = payments.yoomoney_token
adding = False
return cls(
bot=bot,
arSession=arSession,
update=update,
token=token,
adding=adding,
skipping_error=skipping_error,
)
# Уведомления о неработоспособности кассы/кошелька
async def error_notification(self, error_code: str = "Unknown"):
bot_logger.warning("ЮMoney недоступен: %s", error_code)
if not self.skipping_error:
if self.adding and self.update is not None:
await self.update.edit_text(
f"<b>🔮 Не удалось добавить ЮMoney кассу ❌</b>\n"
f"❗️ Ошибка: <code>{error_code}</code>"
)
else:
await send_admins(
self.bot,
f"<b>🔮 ЮMoney недоступен. Как можно быстрее его замените</b>\n"
f"❗️ Ошибка: <code>{error_code}</code>"
)
# Проверка кассы/кошелька
async def check(self) -> str:
status, response = await self._request("account-info")
if status:
if len(response) >= 1:
if response['identified']:
text_identified = "Присутствует"
else:
text_identified = "Отсутствует"
if response['account_status'] == "identified":
text_status = "Идентифицированный счет"
elif response['account_status'] == "anonymous":
text_status = "Анонимный счет"
elif response['account_status'] == "named":
text_status = "Именной счет"
else:
text_status = response['account_status']
if response['account_type'] == "personal":
text_type = "Пользовательский счет"
elif response['account_type'] == "professional":
text_type = "Профессиональный счет"
else:
text_type = response['account_type']
return ded(f"""
<b>🔮 ЮMoney кошелёк полностью функционирует ✅</b>
➖➖➖➖➖➖➖➖➖➖
▪️ Кошелёк: <code>{response['account']}</code>
▪️ Идентификация: <code>{text_identified}</code>
▪️ Статус аккаунта: <code>{text_status}</code>
▪️ Тип счета: <code>{text_type}</code>
""")
return "<b>🔮 Не удалось проверить ЮMoney кошелёк ❌</b>"
# Получение баланса
async def balance(self) -> str:
status, response = await self._request("account-info")
if status:
wallet_balance = response['balance']
wallet_status, wallet_number = await self.account_info()
if wallet_status:
return ded(f"""
<b>🔮 Баланс ЮMoney кошелька составляет</b>
➖➖➖➖➖➖➖➖➖➖
▪️ Кошелёк: <code>{wallet_number}</code>
▪️ Баланс: <code>{wallet_balance}₽</code>
""")
return "<b>🔮 Не удалось получить баланс ЮMoney кошелька ❌</b>"
# Информация об аккаунте
async def account_info(self) -> Tuple[bool, str]:
status, response = await self._request("account-info")
try:
return True, response['account']
except Exception:
bot_logger.warning("Не удалось получить номер ЮMoney", exc_info=True)
return False, ""
# Получение ссылки на авторизацию
async def authorization_get(self) -> str:
if not YOOMONEY_CLIENT_ID:
return "В .env не заполнен YOOMONEY_CLIENT_ID"
session = await self.arSession.get_session()
headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
url = "https://yoomoney.ru/oauth/authorize?" + urlencode({
"client_id": YOOMONEY_CLIENT_ID,
"response_type": "code",
"redirect_uri": "https://yoomoney.ru",
"scope": "account-info operation-history operation-details",
})
response = await session.post(
url=url,
headers=headers,
)
return str(response.url)
# Принятие кода авторизации и получение токена
async def authorization_enter(self, get_code: str) -> Tuple[bool, str, str]:
if not YOOMONEY_CLIENT_ID:
return False, "", "<b>❌ В .env не заполнен YOOMONEY_CLIENT_ID</b>"
session = await self.arSession.get_session()
headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
url = "https://yoomoney.ru/oauth/token?" + urlencode({
"code": get_code,
"client_id": YOOMONEY_CLIENT_ID,
"grant_type": "authorization_code",
"redirect_uri": "https://yoomoney.ru",
})
response = await session.post(
url=url,
headers=headers,
)
response_data = json.loads((await response.read()).decode())
if "error" in response_data:
error = response_data['error']
if error == "invalid_request":
return_message = ded(f"""
<b>❌ Требуемые параметры запроса отсутствуют или имеют неправильные или недопустимые значения</b>
""")
elif error == "unauthorized_client":
return_message = ded(f"""
<b>❌ Недопустимое значение параметра 'client_id' или 'client_secret', или приложение
не имеет права запрашивать авторизацию (например, ЮMoney заблокировал его 'client_id')</b>
""")
elif error == "invalid_grant":
return_message = ded(f"""
<b>❌ В выпуске 'access_token' отказано. ЮMoney не выпускал временный токен,
срок действия токена истек или этот временный токен уже выдан
'access_token' (повторный запрос токена авторизации с тем же временным токеном)</b>
""")
else:
return_message = f"Unknown error: {error}"
return False, "", return_message
elif response_data['access_token'] == "":
return False, "", "<b>❌ Не удалось получить токен. Попробуйте всё снова</b>"
return True, response_data['access_token'], "<b>🔮 ЮMoney кошелёк был успешно изменён ✅</b>"
# Создание счета на оплату
async def bill(self, pay_amount: Union[float, int]) -> Tuple[Union[str, bool], str, str]:
session = await self.arSession.get_session()
bill_receipt = str(gen_id(12))
url = "https://yoomoney.ru/quickpay/confirm.xml?"
wallet_status, wallet_number = await self.account_info()
if wallet_status:
pay_amount_bill = pay_amount + (pay_amount * 0.031)
if float(pay_amount_bill) < 2:
pay_amount_bill = 2.04
payload = {
'receiver': wallet_number,
'quickpay_form': 'button',
'targets': 'Добровольное пожертвование',
'paymentType': 'SB',
'sum': pay_amount_bill,
'label': bill_receipt,
}
for value in payload:
url += str(value).replace("_", "-") + "=" + str(payload[value])
url += "&"
bill_link = str((await session.post(url[:-1].replace(" ", "%20"))).url)
bill_message = ded(f"""
<b>💰 Пополнение баланса</b>
➖➖➖➖➖➖➖➖➖➖
▪️ Для пополнения баланса, нажмите на кнопку ниже
<code>Перейти к оплате</code> и оплатите выставленный вам счёт
▪️ У вас имеется 60 минут на оплату счета
▪️ Сумма пополнения: <code>{pay_amount}₽</code>
➖➖➖➖➖➖➖➖➖➖
❗️ После оплаты, нажмите на <code>Проверить оплату</code>
""")
return bill_message, bill_link, bill_receipt
return False, "", ""
# Проверка счета на оплату
async def bill_check(self, bill_receipt: Optional[Union[str, int]] = None, records: int = 1) -> Tuple[int, float]:
data = {
'type': 'deposition',
'details': 'true',
}
if bill_receipt is not None:
data['label'] = bill_receipt
if records is not None:
data['records'] = str(records)
status, response = await self._request("operation-history", data)
pay_status, pay_amount, pay_currency = 1, 0, 0
if status:
pay_status = 2
if len(response['operations']) >= 1:
pay_currency = response['operations'][0]['amount_currency']
pay_amount = response['operations'][0]['amount']
pay_status = 3
if pay_currency == "RUB":
pay_status = 0
return pay_status, pay_amount
# Генерация запроса
async def _request(
self,
method: str,
data: Optional[Dict[str, Any]] = None,
) -> Tuple[bool, Any]:
session = await self.arSession.get_session()
url = self.base_url + method
try:
response = await session.post(
url=url,
headers=self.headers,
data=data,
)
response_data = json.loads((await response.read()).decode())
if response.status == 200:
return True, response_data
else:
await self.error_notification(f"{response.status} - {str(response_data)}")
return False, response_data
except ClientConnectorCertificateError:
bot_logger.warning("Ошибка SSL при запросе ЮMoney", exc_info=True)
await self.error_notification("CERTIFICATE_VERIFY_FAILED")
return False, "CERTIFICATE_VERIFY_FAILED"
except Exception as ex:
bot_logger.warning("Ошибка запроса ЮMoney", exc_info=True)
await self.error_notification(str(ex))
return False, str(ex)