From 573dae70fbcabfcb15439b9bd13325843b3e3aa2 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 16 Jul 2026 04:40:02 +0500 Subject: [PATCH] feat: add Lolzteam payment integration --- tgbot/data/config.py | 21 +- tgbot/database/db_payments.py | 71 ++++++ tgbot/database/entities.py | 7 + tgbot/database/repository.py | 4 + tgbot/keyboards/inline_admin.py | 37 +++ tgbot/keyboards/inline_admin_products.py | 4 +- tgbot/keyboards/inline_user.py | 6 +- tgbot/keyboards/reply_main.py | 1 + tgbot/routers/admin/admin_payments.py | 172 +++++++++++++ tgbot/routers/user/user_transactions.py | 71 ++++++ tgbot/services/api_cryptobot.py | 115 +++++---- tgbot/services/api_lolzteam.py | 294 +++++++++++++++++++++++ tgbot/utils/const_functions.py | 2 +- 13 files changed, 735 insertions(+), 70 deletions(-) create mode 100644 tgbot/services/api_lolzteam.py diff --git a/tgbot/data/config.py b/tgbot/data/config.py index 80126d0..ed2077f 100644 --- a/tgbot/data/config.py +++ b/tgbot/data/config.py @@ -168,23 +168,4 @@ def validate_bot_config() -> None: def get_text_desc() -> str: from tgbot.utils.const_functions import ded - return ded(f""" - ♻️ Версия бота: {BOT_VERSION} - 👑 Разработчик бота - @djimbox - 🍩 Донат разработчику: Click me - 🤖 Новости и обновления: Click me - 🔗 Тема с ботом [LOLZ]: Click me - """).strip() - - -# Получение варнинг-текста -def get_text_warning() -> str: - from tgbot.utils.const_functions import ded - - return ded(f""" - ❗️ Если вы видите данное сообщение, значит Администратор бота не изменил текст установленный - по умолчанию. Скорее всего данный бот занимается скамом, обманом или другими схожими действиями. - - ❗️ Если вас обманули, не надо писать РАЗРАБОТЧИКУ бота, он ничем не сможет помочь. Бот является - открытым и распространяется публично. Абсолютно любой желающий мог его запустить. - """) + return ded(f"""♻️ Версия бота: {BOT_VERSION}""").strip() diff --git a/tgbot/database/db_payments.py b/tgbot/database/db_payments.py index 4811101..9f5cd50 100644 --- a/tgbot/database/db_payments.py +++ b/tgbot/database/db_payments.py @@ -3,6 +3,7 @@ from typing import Any, Dict, Optional from sqlalchemy import Integer, String, Float from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.ext.asyncio import AsyncConnection from tgbot.database.core import Base from tgbot.database.entities import Payments @@ -22,11 +23,81 @@ class PaymentsModel(Base): String(16), nullable=False, default="False" ) + # Оплата через Lolzteam + lolzteam_token: Mapped[str] = mapped_column(String, nullable=False, default="None") + lolzteam_merchant_id: Mapped[Optional[int]] = mapped_column( + Integer, nullable=True, default=None + ) + status_lolzteam: Mapped[str] = mapped_column( + String(16), nullable=False, default="False" + ) + ModelBase = Payments BaseModel = Payments +async def ensure_payments_schema(conn: AsyncConnection) -> None: + columns = ( + await conn.exec_driver_sql("PRAGMA table_info(storage_payments)") + ).mappings().all() + + if not columns: + return + + merchant_column = next( + (column for column in columns if column["name"] == "lolzteam_merchant_id"), + None, + ) + + if merchant_column is None or merchant_column["notnull"] == 0: + return + + await conn.exec_driver_sql( + "ALTER TABLE storage_payments RENAME TO storage_payments__legacy" + ) + await conn.exec_driver_sql( + """ + CREATE TABLE storage_payments ( + id INTEGER NOT NULL, + cryptobot_token VARCHAR NOT NULL, + stars_course FLOAT NOT NULL, + status_cryptobot VARCHAR(16) NOT NULL, + status_stars VARCHAR(16) NOT NULL, + lolzteam_token VARCHAR NOT NULL, + lolzteam_merchant_id INTEGER, + status_lolzteam VARCHAR(16) NOT NULL, + PRIMARY KEY (id) + ) + """ + ) + await conn.exec_driver_sql( + """ + INSERT INTO storage_payments ( + id, + cryptobot_token, + stars_course, + status_cryptobot, + status_stars, + lolzteam_token, + lolzteam_merchant_id, + status_lolzteam + ) + SELECT + id, + cryptobot_token, + stars_course, + status_cryptobot, + status_stars, + lolzteam_token, + lolzteam_merchant_id, + status_lolzteam + FROM storage_payments__legacy + """ + ) + await conn.exec_driver_sql("DROP TABLE storage_payments__legacy") + + class Paymentsx(BaseRepository[PaymentsModel, Payments]): # Подключение модели платежных настроек def __init__(self): diff --git a/tgbot/database/entities.py b/tgbot/database/entities.py index 2a04f86..a022b7f 100644 --- a/tgbot/database/entities.py +++ b/tgbot/database/entities.py @@ -1,5 +1,6 @@ # - *- coding: utf- 8 - *- from dataclasses import dataclass +from typing import Optional @dataclass @@ -40,6 +41,12 @@ class Payments: status_cryptobot: str status_stars: str + # Оплата через Lolzteam + lolzteam_token: str + lolzteam_merchant_id: Optional[int] + + status_lolzteam: str + @dataclass class Purchase: diff --git a/tgbot/database/repository.py b/tgbot/database/repository.py index 341b9d3..dddf6db 100644 --- a/tgbot/database/repository.py +++ b/tgbot/database/repository.py @@ -162,6 +162,10 @@ async def prepare_database() -> None: import tgbot.database.db_users # noqa: F401 async with engine.begin() as conn: + from tgbot.database.db_payments import ensure_payments_schema + + await ensure_payments_schema(conn) + await conn.run_sync(Base.metadata.create_all) from tgbot.database.db_payments import Paymentsx diff --git a/tgbot/keyboards/inline_admin.py b/tgbot/keyboards/inline_admin.py index 1cf4ec2..7fe60b5 100644 --- a/tgbot/keyboards/inline_admin.py +++ b/tgbot/keyboards/inline_admin.py @@ -98,6 +98,43 @@ async def payment_cryptobot_finl() -> InlineKeyboardMarkup: return keyboard.as_markup() +# Управление - Lolzteam +async def payment_lolzteam_finl() -> InlineKeyboardMarkup: + keyboard = InlineKeyboardBuilder() + + get_payments = await Paymentsx().get() + + if get_payments.lolzteam_token == "None": + assets_symbol = "➖" + else: + assets_symbol = "➕" + + if get_payments.status_lolzteam == "True": + status_kb = ikb( + f"{assets_symbol} | Статус: Включено ✅", + data="payment_lolzteam_status:False", + ) + else: + status_kb = ikb( + f"{assets_symbol} | Статус: Выключено ❌", + data="payment_lolzteam_status:True", + ) + + keyboard.row( + ikb("Информация ♻️", data="payment_lolzteam_check"), + ).row( + ikb("Баланс 💰", data="payment_lolzteam_balance"), + ).row( + ikb("Изменить 🖍", data="payment_lolzteam_edit"), + ).row( + ikb("⁠", data="..."), + ).row( + status_kb, + ) + + return keyboard.as_markup() + + # Управление - Звёзды async def payment_stars_finl() -> InlineKeyboardMarkup: keyboard = InlineKeyboardBuilder() diff --git a/tgbot/keyboards/inline_admin_products.py b/tgbot/keyboards/inline_admin_products.py index b62598e..a6d14bb 100644 --- a/tgbot/keyboards/inline_admin_products.py +++ b/tgbot/keyboards/inline_admin_products.py @@ -23,7 +23,7 @@ async def category_edit_open_finl( ).row( ikb( "▪️ Скопировать ссылку", - copy=f"t.me/{get_bot.username}?start=c_{category_id}", + copy=f"telegram.me/{get_bot.username}?start=c_{category_id}", ), ikb("▪️ Удалить", data=f"category_edit_delete:{category_id}:{remover}"), ).row( @@ -85,7 +85,7 @@ async def position_edit_open_finl( ).row( ikb( "▪️ Скопировать ссылку", - copy=f"t.me/{get_bot.username}?start=p_{position_id}", + copy=f"telegram.me/{get_bot.username}?start=p_{position_id}", ), ikb("▪️ Удалить Позицию", data=f"position_edit_delete:{position_id}:{remover}"), ).row( diff --git a/tgbot/keyboards/inline_user.py b/tgbot/keyboards/inline_user.py index 9b720c1..9141e17 100644 --- a/tgbot/keyboards/inline_user.py +++ b/tgbot/keyboards/inline_user.py @@ -27,7 +27,7 @@ def user_support_finl(support_login: str) -> InlineKeyboardMarkup: keyboard = InlineKeyboardBuilder() keyboard.row( - ikb("💌 Написать в поддержку", url=f"https://t.me/{support_login}"), + ikb("💌 Написать в поддержку", url=f"https://telegram.me/{support_login}"), ) return keyboard.as_markup() @@ -43,6 +43,8 @@ async def refill_method_finl() -> InlineKeyboardMarkup: if get_payments.status_cryptobot == "True": keyboard.row(ikb("🔷 CryptoBot", data="user_refill_method:Cryptobot")) + if get_payments.status_lolzteam == "True": + keyboard.row(ikb("🟢 Lolzteam", data="user_refill_method:Lolzteam")) if get_payments.status_stars == "True": keyboard.row(ikb("⭐️ Звёзды", data="user_refill_method:Stars")) @@ -74,6 +76,8 @@ async def refill_method_buy_finl() -> InlineKeyboardMarkup: if get_payments.status_cryptobot == "True": keyboard.row(ikb("🔷 CryptoBot", data="user_refill_method:Cryptobot")) + if get_payments.status_lolzteam == "True": + keyboard.row(ikb("🟢 Lolzteam", data="user_refill_method:Lolzteam")) if get_payments.status_stars == "True": keyboard.row(ikb("⭐️ Звёзды", data="user_refill_method:Stars")) diff --git a/tgbot/keyboards/reply_main.py b/tgbot/keyboards/reply_main.py index 78a6d5e..52c68bc 100644 --- a/tgbot/keyboards/reply_main.py +++ b/tgbot/keyboards/reply_main.py @@ -38,6 +38,7 @@ def payments_frep() -> ReplyKeyboardMarkup: keyboard.row( rkb("🔷 CryptoBot"), + rkb("🟢 Lolzteam"), rkb("⭐️ Звёзды"), ).row( rkb("🔙 Главное меню"), diff --git a/tgbot/routers/admin/admin_payments.py b/tgbot/routers/admin/admin_payments.py index d927015..358df22 100644 --- a/tgbot/routers/admin/admin_payments.py +++ b/tgbot/routers/admin/admin_payments.py @@ -7,9 +7,11 @@ from tgbot.database import Paymentsx from tgbot.keyboards.inline_admin import ( close_finl, payment_cryptobot_finl, + payment_lolzteam_finl, payment_stars_finl, ) from tgbot.services.api_cryptobot import CryptobotAPI +from tgbot.services.api_lolzteam import LolzteamAPI from tgbot.services.api_stars import StarsAPI from tgbot.utils.const_functions import ded, is_number, to_number from tgbot.utils.misc.bot_models import FSM, ARS @@ -30,6 +32,19 @@ async def payments_cryptobot_open( ) +# Управление - Lolzteam +@router.message(F.text == "🟢 Lolzteam") +async def payments_cryptobot_open( + message: Message, bot: Bot, state: FSM, arSession: ARS +): + await state.clear() + + await message.answer( + "🟢 Управление - Lolzteam", + reply_markup=await payment_lolzteam_finl(), + ) + + # Управление - Звёзды @router.message(F.text == "⭐️ Звёзды") async def payments_stars_open(message: Message, bot: Bot, state: FSM, arSession: ARS): @@ -161,6 +176,163 @@ async def payments_cryptobot_get( ) +################################################################################ +################################### LOLZTEAM ################################## +# Баланс - Lolzteam +@router.callback_query(F.data == "payment_lolzteam_balance") +async def payments_lolzteam_balance( + call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS +): + response = await ( + await LolzteamAPI.connect( + bot=bot, + arSession=arSession, + update=call, + skipping_error=True, + ) + ).balance() + + await call.message.answer( + response, + reply_markup=close_finl(), + ) + + +# Информация - Lolzteam +@router.callback_query(F.data == "payment_lolzteam_check") +async def payment_lolzteam_check( + call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS +): + status, response = await ( + await LolzteamAPI.connect( + bot=bot, + arSession=arSession, + update=call, + skipping_error=True, + ) + ).check() + + await call.message.answer( + response, + reply_markup=close_finl(), + ) + + +# Изменение - Lolzteam +@router.callback_query(F.data == "payment_lolzteam_edit") +async def payment_lolzteam_edit( + call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS +): + await state.set_state("here_lolzteam_token") + await call.message.edit_text( + ded(f""" + 🟢 Изменение Lolzteam - API + ➖➖➖➖➖➖➖➖➖➖ + ▪️ Создайте Приложение в "Lolzteam", сгенерируйте токен (basic, invoice, market) и отправьте + """), + disable_web_page_preview=True, + ) + + +# Выключатель - Lolzteam +@router.callback_query(F.data.startswith("payment_lolzteam_status:")) +async def payment_lolzteam_status( + call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS +): + get_status = call.data.split(":")[1] + get_payments = await Paymentsx().get() + + if get_status == "True" and get_payments.lolzteam_token == "None": + return await call.answer( + "❌ Токен данной платежной системы не был добавлен", True + ) + + await Paymentsx().update(status_lolzteam=get_status) + + await call.message.edit_text( + "🟢 Управление - Lolzteam", + reply_markup=await payment_lolzteam_finl(), + ) + + +############################## ПРИНЯТИЕ LOLZTEAM ############################## +# Принятие токена Lolzteam +@router.message(StateFilter("here_lolzteam_token")) +async def payments_lolzteam_get(message: Message, bot: Bot, state: FSM, arSession: ARS): + get_token = message.text + + cache_message = await message.answer( + "🟢 Проверка введённых Lolzteam данных... 🔄" + ) + + status, response = await ( + await LolzteamAPI.connect( + bot=bot, + arSession=arSession, + update=message, + skipping_error=True, + token=get_token, + ) + ).check() + + if status: + await Paymentsx().update(lolzteam_token=get_token) + + await state.set_state("here_lolzteam_merchant_id") + await cache_message.edit_text( + ded(f""" + 🟢 Изменение Lolzteam - Мерчанты + ➖➖➖➖➖➖➖➖➖➖ + ▪️ Создайте мерчант в "Lolzteam Market" и отправьте его ID + """), + disable_web_page_preview=True, + ) + + else: + await cache_message.edit_text( + "🟢 Не удалось изменить Lolzteam кошелёк ❌" + ) + + +# Принятие айди мерчанта Lolzteam +@router.message(StateFilter("here_lolzteam_merchant_id")) +async def payments_lolzteam_merchant_id_get( + message: Message, bot: Bot, state: FSM, arSession: ARS +): + get_payments = await Paymentsx().get() + get_token = get_payments.lolzteam_token + + get_merchant_id = message.text + + await state.clear() + + cache_message = await message.answer( + "🟢 Проверка введённых Lolzteam данных... 🔄" + ) + + status, response = await ( + await LolzteamAPI.connect( + bot=bot, + arSession=arSession, + update=message, + skipping_error=True, + token=get_token, + merchant_id=get_merchant_id, + ) + ).check() + + if status: + await Paymentsx().update(lolzteam_merchant_id=get_merchant_id) + await cache_message.edit_text("🟢 Lolzteam был успешно изменён ✅") + else: + await cache_message.edit_text("🟢 Не удалось изменить Lolzteam ❌") + + await message.answer( + "🟢 Управление - Lolzteam", + reply_markup=await payment_lolzteam_finl(), + ) + + ################################################################################ #################################### ЗВЁЗДЫ #################################### # Баланс - Звёзды diff --git a/tgbot/routers/user/user_transactions.py b/tgbot/routers/user/user_transactions.py index 0e96387..4d0b497 100644 --- a/tgbot/routers/user/user_transactions.py +++ b/tgbot/routers/user/user_transactions.py @@ -8,6 +8,7 @@ from aiogram.types import CallbackQuery, Message, PreCheckoutQuery from tgbot.database import Paymentsx, Refillx, Userx, Settingsx from tgbot.keyboards.inline_user import refill_bill_finl, refill_method_finl from tgbot.services.api_cryptobot import CryptobotAPI +from tgbot.services.api_lolzteam import LolzteamAPI from tgbot.services.api_stars import StarsAPI from tgbot.utils.const_functions import is_number, to_number, gen_id, ded from tgbot.utils.misc.bot_logging import bot_logger @@ -26,6 +27,7 @@ async def refill_method_list(call: CallbackQuery, bot: Bot, state: FSM, arSessio if ( get_payments.status_cryptobot == "False" + and get_payments.status_lolzteam == "False" and get_payments.status_stars == "False" ): return await call.answer("❗️ Пополнения временно недоступны", True) @@ -49,6 +51,10 @@ async def refill_method_select( return await call.answer( "❌ Пополнение данным способом временно недоступно", True ) + elif refill_method == "Lolzteam" and get_payments.status_lolzteam == "False": + return await call.answer( + "❌ Пополнение данным способом временно недоступно", True + ) elif refill_method == "Stars" and get_payments.status_stars == "False": return await call.answer( "❌ Пополнение данным способом временно недоступно", True @@ -98,6 +104,15 @@ async def refill_amount_get(message: Message, bot: Bot, state: FSM, arSession: A ) ).bill(refill_amount) + elif refill_method == "Lolzteam": + bill_message, bill_link, bill_receipt = await ( + await LolzteamAPI.connect( + bot=bot, + arSession=arSession, + update=cache_message, + ) + ).bill(refill_amount) + elif refill_method == "Stars": bill_message, bill_link, bill_receipt = await ( await StarsAPI.connect( @@ -106,6 +121,7 @@ async def refill_amount_get(message: Message, bot: Bot, state: FSM, arSession: A update=cache_message, ) ).bill(refill_amount) + else: return await cache_message.edit_text( f"❌ Данный способ пополнения не найден. Попробуйте позже: {refill_method}" @@ -180,6 +196,59 @@ async def refill_check_cryptobot( ) +# Проверка оплаты - Lolzteam +@router.callback_query(F.data.startswith("Pay:Lolzteam")) +async def refill_check_lolzteam( + call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS +): + pay_method = call.data.split(":")[1] + pay_comment = call.data.split(":")[2] + + pay_status, pay_amount = await ( + await LolzteamAPI.connect( + bot=bot, + arSession=arSession, + update=call, + ) + ).bill_check(pay_comment) + + if pay_status == 0: + refill_status = await refill_success( + bot=bot, + call=call, + pay_method=pay_method, + pay_amount=pay_amount, + pay_comment=pay_comment, + ) + + if refill_status == "ALREADY": + await call.answer("❗ Ваше пополнение уже зачислено.", True, cache_time=60) + await delete_refill_message( + bot, call.message.chat.id, call.message.message_id + ) + elif refill_status == "USER_NOT_FOUND": + await call.answer( + "❗ Пользователь не найден. Напишите в поддержку.", True, cache_time=30 + ) + elif pay_status == 1: + await call.answer( + "❗️ Не удалось проверить платёж. Попробуйте позже", True, cache_time=30 + ) + elif pay_status == 2: + await call.answer( + "❗️ Оплата не была найдена. Попробуйте позже", True, cache_time=5 + ) + elif pay_status == 3: + await call.answer("❗️ Вы не успели оплатить счёт", True, cache_time=5) + await call.message.edit_reply_markup() + else: + await call.answer( + f"❗ Неизвестная ошибка {pay_status}. Обратитесь в поддержку.", + True, + cache_time=5, + ) + + # Проверка оплаты - Звёзды @router.callback_query(F.data.startswith("Pay:Stars")) async def refill_check_stars(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS): @@ -357,6 +426,8 @@ async def save_refill_success( ) -> Tuple[str, int]: if pay_method == "Cryptobot": text_method = "CryptoBot" + elif pay_method == "Lolzteam": + text_method = "Lolzteam" elif pay_method == "Stars": text_method = "Telegram Stars" else: diff --git a/tgbot/services/api_cryptobot.py b/tgbot/services/api_cryptobot.py index 3ae3f02..6cbb5b0 100644 --- a/tgbot/services/api_cryptobot.py +++ b/tgbot/services/api_cryptobot.py @@ -13,20 +13,31 @@ 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'] +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, + arSession: ARS, + update: Optional[Union[Message, CallbackQuery]] = None, + token: str = "None", + adding: bool = False, + skipping_error: bool = False, ): self.bot = bot self.arSession = arSession @@ -38,12 +49,12 @@ class CryptobotAPI: # Инициализация данных @classmethod async def connect( - cls, - bot: Bot, - arSession: ARS, - update: Optional[Union[Message, CallbackQuery]] = None, - token: Optional[str] = None, - skipping_error: bool = False, + 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 @@ -75,21 +86,23 @@ class CryptobotAPI: await send_admins( self.bot, f"🔷 CryptoBot недоступен. Как можно быстрее его замените\n" - f"❗️ Ошибка: {error_code}" + f"❗️ Ошибка: {error_code}", ) # Проверка кассы/кошелька async def check(self) -> Tuple[bool, str]: status, response = await self._request("getMe") - if status and response['ok']: - return True, ded(f""" + if status and response["ok"]: + return True, ded( + f""" 🔷 CryptoBot кошелёк полностью функционирует ✅ ➖➖➖➖➖➖➖➖➖➖ ▪️ Токен: {self.token} ▪️ Айди: {response['result']['app_id']} ▪️ Имя: {response['result']['name']} - """) + """ + ) return False, "🔷 Не удалось проверить CryptoBot кошелёк ❌" @@ -97,17 +110,17 @@ class CryptobotAPI: async def balance(self) -> str: status, response = await self._request("getBalance") - if status and response['ok']: + if status and response["ok"]: save_currencies = [] response_balances = sorted( - response['result'], + response["result"], reverse=True, - key=lambda balance: to_number(balance['available']), + key=lambda balance: to_number(balance["available"]), ) for currency in response_balances: - if currency['currency_code'] in ALLOW_CURRENCIES: + if currency["currency_code"] in ALLOW_CURRENCIES: save_currencies.append( f"▪️ {currency['currency_code']}: {currency['available']}" ) @@ -123,24 +136,26 @@ class CryptobotAPI: return "🔷 Не удалось получить баланс CryptoBot кошелька ❌" # Создание счета на оплату - async def bill(self, pay_amount: Union[float, int]) -> Tuple[Union[str, bool], str, str]: + 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 + "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']: + if status and response["ok"]: bill_message = ded(f""" 💰 Пополнение баланса ➖➖➖➖➖➖➖➖➖➖ - ▪️ Для пополнения баланса, нажмите на кнопку ниже + ▪️ Для пополнения баланса, нажмите на кнопку ниже Перейти к оплате и оплатите выставленный вам счёт ▪️ У вас имеется 3 часа на оплату счета ▪️ Сумма пополнения: {pay_amount}₽ @@ -148,46 +163,52 @@ class CryptobotAPI: ❗️ После оплаты, нажмите на Проверить оплату """) - return bill_message, response['result']['mini_app_invoice_url'], response['result']['invoice_id'] + 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]: + 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', + "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 status and response["ok"]: + get_invoice = response["result"]["items"][0] - if get_invoice['status'] == "active": + if get_invoice["status"] == "active": pay_status = 2 - elif get_invoice['status'] == "expired": + elif get_invoice["status"] == "expired": pay_status = 3 else: pay_status = 0 - pay_amount = to_number(get_invoice['amount']) + pay_amount = to_number(get_invoice["amount"]) return pay_status, pay_amount # Генерация запроса async def _request( - self, - method: str, - data: Optional[Dict[str, Any]] = None, + 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/' + base_url = "https://pay.crypt.bot/api/" headers = { - 'Crypto-Pay-API-Token': self.token, - 'Content-Type': 'application/x-www-form-urlencoded', + "Crypto-Pay-API-Token": self.token, + "Content-Type": "application/x-www-form-urlencoded", } url = base_url + method @@ -204,7 +225,9 @@ class CryptobotAPI: if response.status == 200: return True, response_data else: - await self.error_notification(f"{response.status} - {str(response_data)}") + await self.error_notification( + f"{response.status} - {str(response_data)}" + ) return False, response_data except ClientConnectorCertificateError: diff --git a/tgbot/services/api_lolzteam.py b/tgbot/services/api_lolzteam.py new file mode 100644 index 0000000..c24efe8 --- /dev/null +++ b/tgbot/services/api_lolzteam.py @@ -0,0 +1,294 @@ +# - *- 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, get_unix, 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 + +# Список поддерживаемых валют +ALLOW_CURRENCIES = [ + "rub", + "uah", + "kzt", + "byn", + "usd", + "eur", + "gbp", + "cny", + "try", + "jpy", + "brl", +] + + +# АПИ для работы с Lolzteam +class LolzteamAPI: + # Настройка клиента Lolzteam + def __init__( + self, + bot: Bot, + arSession: ARS, + update: Optional[Union[Message, CallbackQuery]] = None, + token: str = "None", + merchant_id: int = "None", + adding: bool = False, + skipping_error: bool = False, + ): + self.bot = bot + self.arSession = arSession + self.update = update + self.token = token + self.merchant_id = merchant_id + 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, + merchant_id: Optional[int] = None, + skipping_error: bool = False, + ) -> "LolzteamAPI": + adding = token is not None + + if token is None or merchant_id is None: + payments = await Paymentsx().get() + adding = False + + if token is None: + token = payments.lolzteam_token + + if merchant_id is None: + merchant_id = payments.lolzteam_merchant_id + + return cls( + bot=bot, + arSession=arSession, + update=update, + token=token, + merchant_id=merchant_id, + adding=adding, + skipping_error=skipping_error, + ) + + # Уведомления о неработоспособности кассы/кошелька + async def error_notification(self, error_code: str = "Unknown"): + bot_logger.warning("Lolzteam недоступен: %s", error_code) + + if not self.skipping_error: + if self.adding and self.update is not None: + await self.update.edit_text( + f"🟢 Не удалось добавить Lolzteam кассу ❌\n" + f"❗️ Ошибка: {error_code}" + ) + else: + await send_admins( + self.bot, + f"🟢 Lolzteam недоступен. Как можно быстрее его замените\n" + f"❗️ Ошибка: {error_code}", + ) + + # Проверка кассы/кошелька + async def check(self) -> Tuple[bool, str]: + status, response = await self._request("me", method="GET") + merchant_valid = False + + if status and "errors" not in response and self.merchant_id: + balances = response["user"]["balances"] + + for balance in balances: + if int(self.merchant_id) == balance["merchant_id"]: + merchant_valid = True + + if merchant_valid: + return True, ded( + f""" + 🟢 Lolzteam полностью функционирует ✅ + ➖➖➖➖➖➖➖➖➖➖ + ▪️ Токен: {self.token} + ▪️ Айди: {response['user']['user_id']} + ▪️ Айди Мерчанта: {self.merchant_id} + """ + ) + + elif status and "errors" not in response and not self.merchant_id: + return True, ded( + f""" + 🟢 Lolzteam полностью функционирует ✅ + ➖➖➖➖➖➖➖➖➖➖ + ▪️ Токен: {self.token} + ▪️ Айди: {response['user']['user_id']} + """ + ) + + return False, "🟢 Не удалось проверить Lolzteam ❌" + + # Получение баланса + async def balance(self) -> str: + status, response = await self._request("me", method="GET") + + if status and "errors" not in response: + merchants = [] + + response_balances = sorted( + response["user"]["balances"], + reverse=True, + key=lambda balance: to_number(balance["balance"]), + ) + + for balance in response_balances: + if balance["merchant_id"] == self.merchant_id: + title = balance["title"] + custom_title = balance["custom_title"] + + merchants.append( + f"▪️ {title if not custom_title else custom_title}: {balance['balance']} ₽" + ) + + merchants = "\n".join(merchants) + + return ded(f""" + 🟢 Баланс Lolzteam составляет + ➖➖➖➖➖➖➖➖➖➖ + {merchants} + """) + + return "🟢 Не удалось получить баланс Lolzteam ❌" + + # Создание счета на оплату + async def bill( + self, pay_amount: Union[float, int] + ) -> Tuple[Union[str, bool], str, str]: + + pro_payment_id = get_unix() + eu_payment_id = gen_id(12) + + payment_id = str(pro_payment_id) + str(eu_payment_id) + + bot_data = await self.bot.get_me() + bot_username = bot_data.username + bot_url = f"https://telegram.me/{bot_username}" + + payload = { + "currency": "rub", + "amount": pay_amount, + "payment_id": payment_id, + "comment": f"Пополнение #{payment_id}", + "url_success": bot_url, + "merchant_id": self.merchant_id, + "lifetime": 10800, + "is_test": True, # Тестовый режим для отладки + } + + status, response = await self._request("invoice", payload) + + if status and "errors" not in response: + bill_message = ded(f""" + 💰 Пополнение баланса + ➖➖➖➖➖➖➖➖➖➖ + ▪️ Для пополнения баланса, нажмите на кнопку ниже + Перейти к оплате и оплатите выставленный вам счёт + ▪️ У вас имеется 3 часа на оплату счета + ▪️ Сумма пополнения: {pay_amount}₽ + ➖➖➖➖➖➖➖➖➖➖ + ❗️ После оплаты, нажмите на Проверить оплату + """) + + return ( + bill_message, + response["invoice"]["url"], + response["invoice"]["invoice_id"], + ) + + return False, "", "" + + # Проверка счета на оплату + async def bill_check( + self, bill_receipt: Optional[Union[str, int]] = None, records: int = 1 + ) -> Tuple[int, float]: + payload = {"invoice_id": int(bill_receipt)} + + status, response = await self._request("invoice", payload, method="GET") + + pay_status, pay_amount = 1, 0 + + if status and "errors" not in response: + get_invoice = response["invoice"] + status = get_invoice["status"] + + time = response["system_info"]["time"] + expires_at = get_invoice["expires_at"] + + # not_paid + time < expires_at + if (status == "not_paid") and (time < expires_at): + pay_status = 2 + + # not_paid + time > expires_at + elif (status == "expired") and (time > expires_at): + pay_status = 3 + + # paid + else: + pay_status = 0 + pay_amount = to_number(get_invoice["amount"]) + + return pay_status, pay_amount + + # Генерация запроса + async def _request( + self, + path: str, + data: Optional[Dict[str, Any]] = None, + method: str = "POST", + ) -> Tuple[bool, Any]: + session = await self.arSession.get_session() + method = method.upper() + + base_url = "https://prod-api.lzt.market/" + headers = { + "Authorization": f"Bearer {self.token}", + "Accept": "application/json", + } + + url = base_url + path + + try: + if method == "POST": + response = await session.post(url=url, headers=headers, data=data) + + elif method == "GET": + response = await session.get(url=url, headers=headers, params=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 при запросе Lolzteam", exc_info=True) + await self.error_notification("CERTIFICATE_VERIFY_FAILED") + + return False, "CERTIFICATE_VERIFY_FAILED" + + except Exception as ex: + bot_logger.warning("Ошибка запроса Lolzteam", exc_info=True) + await self.error_notification(str(ex)) + + return False, str(ex) diff --git a/tgbot/utils/const_functions.py b/tgbot/utils/const_functions.py index 7795ea4..871668f 100644 --- a/tgbot/utils/const_functions.py +++ b/tgbot/utils/const_functions.py @@ -48,7 +48,7 @@ def ikb( if copy is not None: return InlineKeyboardButton(text=text, copy_text=CopyTextButton(text=copy)) if login is not None: - return InlineKeyboardButton(text=text, url=f"https://t.me/{login}") + return InlineKeyboardButton(text=text, url=f"https://telegram.me/{login}") raise ValueError("Не указано действие для inline-кнопки")