Merge branch 'Vladless:main' into main
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
- v2.3.1 — стабильная версия подписок вместо ключей с кнопками автодобавления в приложение:
|
||||
|
||||

|
||||
- v3.0.4_beta — полноценные подписки с выбором серверов из клиентского приложения (не в боте):
|
||||
- v3.1 — настолько большая, что смотрите в [релизе](https://github.com/Vladless/Solo_bot/releases/tag/v3.1):
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import uuid
|
||||
from urllib.parse import urlencode
|
||||
import hashlib
|
||||
|
||||
from aiogram import F, Router, types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
@@ -7,12 +6,13 @@ 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 robokassa import HashAlgorithm, Robokassa
|
||||
|
||||
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
|
||||
from logger import logger
|
||||
|
||||
router = Router()
|
||||
|
||||
@@ -20,44 +20,45 @@ 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
|
||||
robokassa = Robokassa(
|
||||
merchant_login=ROBOKASSA_LOGIN,
|
||||
password1=ROBOKASSA_PASSWORD1,
|
||||
password2=ROBOKASSA_PASSWORD2,
|
||||
algorithm=HashAlgorithm.md5,
|
||||
is_test=ROBOKASSA_TEST_MODE,
|
||||
)
|
||||
|
||||
data = ":".join(map(str, args))
|
||||
return hashlib.sha256(data.encode("utf-8")).hexdigest()
|
||||
logger.info("Robokassa initialized with login: {}", ROBOKASSA_LOGIN)
|
||||
|
||||
|
||||
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)}"
|
||||
def generate_payment_link(amount, inv_id, description):
|
||||
"""Генерация ссылки на оплату."""
|
||||
logger.debug(
|
||||
f"Generating payment link for amount: {amount}, inv_id: {inv_id}, description: {description}"
|
||||
)
|
||||
payment_link = robokassa._payment.link.generate_by_script(
|
||||
out_sum=amount,
|
||||
inv_id=inv_id,
|
||||
description="description",
|
||||
)
|
||||
logger.info(f"Generated payment link: {payment_link}")
|
||||
return payment_link
|
||||
|
||||
|
||||
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:
|
||||
logger.debug(
|
||||
f"Deleting previous message with ID: {previous_message_id}"
|
||||
)
|
||||
await bot.delete_message(
|
||||
chat_id=chat_id, message_id=previous_message_id
|
||||
)
|
||||
@@ -67,6 +68,7 @@ async def send_message_with_deletion(
|
||||
)
|
||||
await state.update_data({message_key: sent_message.message_id})
|
||||
|
||||
logger.debug(f"Sent new message with ID: {sent_message.message_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении/отправке сообщения: {e}")
|
||||
return None
|
||||
@@ -79,6 +81,7 @@ async def process_callback_pay_robokassa(
|
||||
callback_query: types.CallbackQuery, state: FSMContext
|
||||
):
|
||||
tg_id = callback_query.from_user.id
|
||||
logger.info(f"User {tg_id} initiated Robokassa payment.")
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
for i in range(0, len(PAYMENT_OPTIONS), 2):
|
||||
@@ -86,18 +89,18 @@ async def process_callback_pay_robokassa(
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=PAYMENT_OPTIONS[i]["text"],
|
||||
callback_data=f'robokassa_{PAYMENT_OPTIONS[i]["callback_data"]}',
|
||||
callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}',
|
||||
),
|
||||
InlineKeyboardButton(
|
||||
text=PAYMENT_OPTIONS[i + 1]["text"],
|
||||
callback_data=f'robokassa_{PAYMENT_OPTIONS[i + 1]["callback_data"]}',
|
||||
callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i + 1]["callback_data"]}',
|
||||
),
|
||||
)
|
||||
else:
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=PAYMENT_OPTIONS[i]["text"],
|
||||
callback_data=f'robokassa_{PAYMENT_OPTIONS[i]["callback_data"]}',
|
||||
callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}',
|
||||
)
|
||||
)
|
||||
builder.row(
|
||||
@@ -113,11 +116,13 @@ async def process_callback_pay_robokassa(
|
||||
exists = await check_connection_exists(tg_id)
|
||||
if not exists:
|
||||
await add_connection(tg_id, balance=0.0, trial=0)
|
||||
logger.info(f"Created new connection for user {tg_id} with balance 0.0.")
|
||||
|
||||
try:
|
||||
await bot.delete_message(
|
||||
chat_id=tg_id, message_id=callback_query.message.message_id
|
||||
)
|
||||
logger.debug(f"Deleted message with ID: {callback_query.message.message_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось удалить сообщение: {e}")
|
||||
|
||||
@@ -127,6 +132,7 @@ async def process_callback_pay_robokassa(
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
await state.set_state(ReplenishBalanceState.choosing_amount_robokassa)
|
||||
logger.info(f"Displayed amount selection for user {tg_id}.")
|
||||
await callback_query.answer()
|
||||
|
||||
|
||||
@@ -134,39 +140,50 @@ async def process_callback_pay_robokassa(
|
||||
async def process_amount_selection(
|
||||
callback_query: types.CallbackQuery, state: FSMContext
|
||||
):
|
||||
data = callback_query.data.split("|", 1)
|
||||
if len(data) != 2:
|
||||
logger.info(f"Получены данные callback_data: {callback_query.data}")
|
||||
|
||||
data = callback_query.data.split("|")
|
||||
if len(data) != 3 or data[1] != "amount":
|
||||
logger.error("Ошибка: callback_data не соответствует формату.")
|
||||
await send_message_with_deletion(
|
||||
callback_query.from_user.id,
|
||||
"Неверные данные для выбора суммы.",
|
||||
chat_id=callback_query.from_user.id,
|
||||
text="Неверные данные для выбора суммы.",
|
||||
state=state,
|
||||
)
|
||||
await callback_query.answer("Ошибка: данные повреждены.")
|
||||
return
|
||||
|
||||
amount_str = data[1]
|
||||
amount_str = data[2]
|
||||
try:
|
||||
amount = int(amount_str)
|
||||
except ValueError:
|
||||
if amount <= 0:
|
||||
raise ValueError("Сумма должна быть положительным числом.")
|
||||
except ValueError as e:
|
||||
logger.error(f"Некорректное значение суммы: {amount_str}. Ошибка: {e}")
|
||||
await send_message_with_deletion(
|
||||
callback_query.from_user.id, "Некорректная сумма.", state=state
|
||||
chat_id=callback_query.from_user.id,
|
||||
text="Некорректная сумма. Попробуйте снова.",
|
||||
state=state,
|
||||
)
|
||||
await callback_query.answer("Некорректная сумма.")
|
||||
return
|
||||
|
||||
await state.update_data(amount=amount)
|
||||
inv_id = uuid.uuid4().hex[:8]
|
||||
logger.info(f"User {callback_query.from_user.id} selected amount: {amount}.")
|
||||
|
||||
tg_id = callback_query.from_user.id
|
||||
payment_url = generate_payment_link(
|
||||
ROBOKASSA_LOGIN,
|
||||
ROBOKASSA_PASSWORD1,
|
||||
amount,
|
||||
inv_id,
|
||||
tg_id,
|
||||
"Пополнение баланса",
|
||||
ROBOKASSA_TEST_MODE,
|
||||
)
|
||||
|
||||
logger.info(f"Payment URL for user {callback_query.from_user.id}: {payment_url}")
|
||||
|
||||
confirm_keyboard = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text="Оплатить", url=payment_url)],
|
||||
[InlineKeyboardButton(text="⬅️ Назад", callback_data="pay")],
|
||||
[InlineKeyboardButton(text="⬅️ Назад", callback_data="pay_robokassa")],
|
||||
]
|
||||
)
|
||||
|
||||
@@ -174,31 +191,58 @@ async def process_amount_selection(
|
||||
text=f"Вы выбрали пополнение на {amount} рублей. Для оплаты перейдите по ссылке ниже:",
|
||||
reply_markup=confirm_keyboard,
|
||||
)
|
||||
logger.info(f"Payment link sent to user {callback_query.from_user.id}.")
|
||||
await callback_query.answer()
|
||||
|
||||
|
||||
async def robokassa_webhook(request):
|
||||
"""Обработка уведомлений от Робокассы."""
|
||||
params = dict(await request.post())
|
||||
logger.debug(f"Webhook event received: {params}")
|
||||
"""Обработка webhook-уведомлений от Robokassa."""
|
||||
try:
|
||||
params = await request.post()
|
||||
logger.debug(f"Received webhook params: {params}")
|
||||
|
||||
received_signature = params.get("SignatureValue")
|
||||
amount = params.get("OutSum")
|
||||
inv_id = params.get("InvId")
|
||||
if not check_payment_signature(params):
|
||||
logger.error("Неверная подпись или данные запроса.")
|
||||
return web.Response(status=400)
|
||||
|
||||
signature = calculate_signature(amount, inv_id, ROBOKASSA_PASSWORD2)
|
||||
amount = params.get("OutSum")
|
||||
inv_id = params.get("InvId")
|
||||
|
||||
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))
|
||||
if not amount or not inv_id:
|
||||
logger.error("Отсутствуют обязательные параметры.")
|
||||
return web.Response(status=400)
|
||||
|
||||
tg_id = inv_id
|
||||
|
||||
await update_balance(int(tg_id), float(amount))
|
||||
await send_payment_success_notification(tg_id, float(amount))
|
||||
|
||||
logger.info(f"Payment successful. Balance updated for user {tg_id}.")
|
||||
return web.Response(text=f"OK{inv_id}")
|
||||
else:
|
||||
return web.Response(status=400)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing webhook: {e}")
|
||||
return web.Response(status=500)
|
||||
|
||||
|
||||
def check_payment_signature(params):
|
||||
"""Проверка подписи запроса от Robokassa."""
|
||||
out_sum = params.get("OutSum")
|
||||
inv_id = params.get("InvId")
|
||||
signature_value = params.get("SignatureValue")
|
||||
|
||||
signature_string = f"{out_sum}:{inv_id}:{ROBOKASSA_PASSWORD2}"
|
||||
|
||||
expected_signature = (
|
||||
hashlib.md5(signature_string.encode("utf-8")).hexdigest().upper()
|
||||
)
|
||||
|
||||
logger.debug(f"Expected signature: {expected_signature}")
|
||||
logger.debug(f"Received signature: {signature_value}")
|
||||
|
||||
return signature_value == expected_signature
|
||||
|
||||
|
||||
async def send_payment_success_notification(user_id: int, amount: float):
|
||||
"""Уведомление о успешном пополнении."""
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile")
|
||||
@@ -209,3 +253,59 @@ async def send_payment_success_notification(user_id: int, amount: float):
|
||||
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
logger.info(f"Sent payment success notification to user {user_id}.")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "enter_custom_amount_robokassa")
|
||||
async def process_custom_amount_selection(
|
||||
callback_query: types.CallbackQuery, state: FSMContext
|
||||
):
|
||||
tg_id = callback_query.from_user.id
|
||||
logger.info(f"User {tg_id} chose to enter a custom amount.")
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
text="Пожалуйста, введите сумму пополнения в рублях (например, 150):"
|
||||
)
|
||||
await state.set_state(
|
||||
ReplenishBalanceState.waiting_for_payment_confirmation_robokassa
|
||||
)
|
||||
await callback_query.answer()
|
||||
|
||||
|
||||
@router.message(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa)
|
||||
async def handle_custom_amount_input(message: types.Message, state: FSMContext):
|
||||
tg_id = message.from_user.id
|
||||
logger.info(f"User {tg_id} entered custom amount: {message.text}")
|
||||
|
||||
try:
|
||||
amount = int(message.text)
|
||||
if amount <= 0:
|
||||
raise ValueError("Сумма должна быть положительным числом.")
|
||||
|
||||
await state.update_data(amount=amount)
|
||||
|
||||
payment_url = generate_payment_link(
|
||||
amount,
|
||||
tg_id,
|
||||
"Пополнение баланса",
|
||||
)
|
||||
|
||||
logger.info(f"Generated payment link for user {tg_id}: {payment_url}")
|
||||
|
||||
confirm_keyboard = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text="Оплатить", url=payment_url)],
|
||||
[InlineKeyboardButton(text="⬅️ Назад", callback_data="pay_robokassa")],
|
||||
]
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
text=f"Вы выбрали пополнение на {amount} рублей. Для оплаты перейдите по ссылке ниже:",
|
||||
reply_markup=confirm_keyboard,
|
||||
)
|
||||
await state.clear()
|
||||
except ValueError as e:
|
||||
logger.error(f"Некорректная сумма от пользователя {tg_id}: {e}")
|
||||
await message.answer(
|
||||
text="Введите корректную сумму в рублях (целое положительное число)."
|
||||
)
|
||||
|
||||
+1
-1
@@ -135,7 +135,7 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session):
|
||||
async def handle_about_vpn(callback_query: CallbackQuery):
|
||||
await callback_query.message.delete()
|
||||
|
||||
about_vpn_message = get_about_vpn("3.0.8_preStable")
|
||||
about_vpn_message = get_about_vpn("3.1.0_Stable")
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(
|
||||
|
||||
Reference in New Issue
Block a user