From 2961477ecf4e8a648ff0841a2f36eeee2075d2c8 Mon Sep 17 00:00:00 2001 From: Vladless Date: Fri, 15 Nov 2024 15:07:51 +0300 Subject: [PATCH] robokassa_beta --- bot.py | 6 +- handlers/pay.py | 9 +- handlers/payments/cryprobot_pay.py | 1 - handlers/payments/robokassa_pay.py | 211 +++++++++++++++++++++++++++++ 4 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 handlers/payments/robokassa_pay.py diff --git a/bot.py b/bot.py index c01c2d8d..9b47ffe5 100644 --- a/bot.py +++ b/bot.py @@ -1,7 +1,7 @@ from aiogram import Bot, Dispatcher, Router from aiogram.fsm.storage.memory import MemoryStorage -from config import API_TOKEN, CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, STARS_ENABLE, YOOKASSA_ENABLE +from config import API_TOKEN, CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, ROBOKASSA_ENABLE, STARS_ENABLE, YOOKASSA_ENABLE from middlewares.database import DatabaseMiddleware from middlewares.logging import LoggingMiddleware @@ -13,7 +13,7 @@ router = Router() from handlers import commands, coupons, notifications, pay, profile, start from handlers.admin import admin_commands, admin_coupons, admin_panel, admin_user_editor from handlers.keys import key_management, keys -from handlers.payments import cryprobot_pay, freekassa_pay, stars_pay, yookassa_pay +from handlers.payments import cryprobot_pay, freekassa_pay, robokassa_pay, stars_pay, yookassa_pay dp.include_router(admin_commands.router) dp.include_router(admin_coupons.router) @@ -34,6 +34,8 @@ if CRYPTO_BOT_ENABLE: dp.include_router(cryprobot_pay.router) if STARS_ENABLE: dp.include_router(stars_pay.router) +if ROBOKASSA_ENABLE: + dp.include_router(robokassa_pay.router) dp.include_router(notifications.router) dp.message.middleware(LoggingMiddleware()) diff --git a/handlers/pay.py b/handlers/pay.py index 42ebc494..c09cb66e 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -2,7 +2,7 @@ from aiogram import F, Router from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from config import CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, STARS_ENABLE, YOOKASSA_ENABLE +from config import CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, ROBOKASSA_ENABLE, STARS_ENABLE, YOOKASSA_ENABLE from database import get_trial from handlers.start import send_welcome_message @@ -42,6 +42,13 @@ async def handle_pay(callback_query: CallbackQuery): callback_data="pay_stars", ) ) + if ROBOKASSA_ENABLE: + builder.row( + InlineKeyboardButton( + text="⭐ RoboKassa", + callback_data="pay_robokassa", + ) + ) builder.row( InlineKeyboardButton( text="🎟️ Активировать купон", callback_data="activate_coupon" diff --git a/handlers/payments/cryprobot_pay.py b/handlers/payments/cryprobot_pay.py index 1f47613e..9f95bc2f 100644 --- a/handlers/payments/cryprobot_pay.py +++ b/handlers/payments/cryprobot_pay.py @@ -170,7 +170,6 @@ async def process_amount_selection( await callback_query.answer() - async def send_payment_success_notification(user_id: int, amount: float): try: builder = InlineKeyboardBuilder() diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py new file mode 100644 index 00000000..db6d3762 --- /dev/null +++ b/handlers/payments/robokassa_pay.py @@ -0,0 +1,211 @@ +import uuid +from urllib.parse import urlencode + +from aiogram import F, Router, types +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from aiogram.utils.keyboard import InlineKeyboardBuilder +from aiohttp import web +from loguru import logger + +from bot import bot +from config import ROBOKASSA_LOGIN, ROBOKASSA_PASSWORD1, ROBOKASSA_PASSWORD2, ROBOKASSA_TEST_MODE +from database import add_connection, check_connection_exists, get_key_count, update_balance +from handlers.texts import PAYMENT_OPTIONS + +router = Router() + + +class ReplenishBalanceState(StatesGroup): + choosing_amount_robokassa = State() + waiting_for_payment_confirmation_robokassa = State() + entering_custom_amount_robokassa = State() + + +def calculate_signature(*args) -> str: + """Создание подписи SHA-256 для Робокассы.""" + import hashlib + + data = ":".join(map(str, args)) + return hashlib.sha256(data.encode("utf-8")).hexdigest() + + +def generate_payment_link( + merchant_login, password1, cost, inv_id, description, is_test +): + """Генерация ссылки для оплаты через Робокассу.""" + signature = calculate_signature(merchant_login, cost, inv_id, password1) + payment_url = "https://auth.robokassa.ru/Merchant/Index.aspx" + params = { + "MerchantLogin": merchant_login, + "OutSum": cost, + "InvId": inv_id, + "Description": description, + "SignatureValue": signature, + "IsTest": int(is_test), + } + return f"{payment_url}?{urlencode(params)}" + + +async def send_message_with_deletion( + chat_id, text, reply_markup=None, state=None, message_key="last_message_id" +): + """Отправка сообщения с удалением предыдущего.""" + if state: + try: + state_data = await state.get_data() + previous_message_id = state_data.get(message_key) + + if previous_message_id: + await bot.delete_message( + chat_id=chat_id, message_id=previous_message_id + ) + + sent_message = await bot.send_message( + chat_id=chat_id, text=text, reply_markup=reply_markup + ) + await state.update_data({message_key: sent_message.message_id}) + + except Exception as e: + logger.error(f"Ошибка при удалении/отправке сообщения: {e}") + return None + + return sent_message + + +@router.callback_query(F.data == "pay_robokassa") +async def process_callback_pay_robokassa( + callback_query: types.CallbackQuery, state: FSMContext +): + tg_id = callback_query.from_user.id + + builder = InlineKeyboardBuilder() + for i in range(0, len(PAYMENT_OPTIONS), 2): + if i + 1 < len(PAYMENT_OPTIONS): + builder.row( + InlineKeyboardButton( + text=PAYMENT_OPTIONS[i]["text"], + callback_data=f'robokassa_{PAYMENT_OPTIONS[i]["callback_data"]}', + ), + InlineKeyboardButton( + text=PAYMENT_OPTIONS[i + 1]["text"], + callback_data=f'robokassa_{PAYMENT_OPTIONS[i + 1]["callback_data"]}', + ), + ) + else: + builder.row( + InlineKeyboardButton( + text=PAYMENT_OPTIONS[i]["text"], + callback_data=f'robokassa_{PAYMENT_OPTIONS[i]["callback_data"]}', + ) + ) + builder.row( + InlineKeyboardButton( + text="💰 Ввести свою сумму", callback_data="enter_custom_amount_robokassa" + ) + ) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile")) + + key_count = await get_key_count(tg_id) + + if key_count == 0: + exists = await check_connection_exists(tg_id) + if not exists: + await add_connection(tg_id, balance=0.0, trial=0) + + try: + await bot.delete_message( + chat_id=tg_id, message_id=callback_query.message.message_id + ) + except Exception as e: + logger.error(f"Не удалось удалить сообщение: {e}") + + await bot.send_message( + chat_id=tg_id, + text="Выберите сумму пополнения:", + reply_markup=builder.as_markup(), + ) + await state.set_state(ReplenishBalanceState.choosing_amount_robokassa) + await callback_query.answer() + + +@router.callback_query(F.data.startswith("robokassa_amount|")) +async def process_amount_selection( + callback_query: types.CallbackQuery, state: FSMContext +): + data = callback_query.data.split("|", 1) + if len(data) != 2: + await send_message_with_deletion( + callback_query.from_user.id, + "Неверные данные для выбора суммы.", + state=state, + ) + return + + amount_str = data[1] + try: + amount = int(amount_str) + except ValueError: + await send_message_with_deletion( + callback_query.from_user.id, "Некорректная сумма.", state=state + ) + return + + await state.update_data(amount=amount) + inv_id = uuid.uuid4().hex[:8] + payment_url = generate_payment_link( + ROBOKASSA_LOGIN, + ROBOKASSA_PASSWORD1, + amount, + inv_id, + "Пополнение баланса", + ROBOKASSA_TEST_MODE, + ) + + confirm_keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Оплатить", url=payment_url)], + [InlineKeyboardButton(text="⬅️ Назад", callback_data="pay")], + ] + ) + + await callback_query.message.edit_text( + text=f"Вы выбрали пополнение на {amount} рублей. Для оплаты перейдите по ссылке ниже:", + reply_markup=confirm_keyboard, + ) + await callback_query.answer() + + +async def robokassa_webhook(request): + """Обработка уведомлений от Робокассы.""" + params = dict(await request.post()) + logger.debug(f"Webhook event received: {params}") + + received_signature = params.get("SignatureValue") + amount = params.get("OutSum") + inv_id = params.get("InvId") + + signature = calculate_signature(amount, inv_id, ROBOKASSA_PASSWORD2) + + if signature.lower() == received_signature.lower(): + user_id = params.get("Shp_user_id") + await update_balance(int(user_id), float(amount)) + await send_payment_success_notification(user_id, float(amount)) + return web.Response(text=f"OK{inv_id}") + else: + return web.Response(status=400) + + +async def send_payment_success_notification(user_id: int, amount: float): + """Уведомление о успешном пополнении.""" + builder = InlineKeyboardBuilder() + builder.row( + InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile") + ) + + await bot.send_message( + chat_id=user_id, + text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!", + reply_markup=builder.as_markup(), + )