Clear code

This commit is contained in:
Boris Kovalskii
2025-07-24 09:42:23 +10:00
committed by GitHub
parent c877aef711
commit c12e61fb41
3 changed files with 31 additions and 105 deletions
+7 -7
View File
@@ -65,6 +65,10 @@ async def handle_pay(callback_query: CallbackQuery, state: FSMContext, session:
payment_handlers.append(process_callback_pay_yookassa)
if YOOMONEY_ENABLE:
payment_handlers.append(process_callback_pay_yoomoney)
if KASSAI_ENABLE:
payment_handlers.append(process_callback_pay_kassai)
if WATA_RU_ENABLE or WATA_SBP_ENABLE or WATA_INT_ENABLE:
payment_handlers.append(process_callback_pay_wata)
if CRYPTO_BOT_ENABLE:
payment_handlers.append(process_callback_pay_cryptobot)
if STARS_ENABLE:
@@ -73,10 +77,6 @@ async def handle_pay(callback_query: CallbackQuery, state: FSMContext, session:
payment_handlers.append(process_callback_pay_robokassa)
if FREEKASSA_ENABLE:
payment_handlers.append(process_callback_pay_freekassa)
if KASSAI_ENABLE:
payment_handlers.append(process_callback_pay_kassai)
if WATA_RU_ENABLE or WATA_SBP_ENABLE or WATA_INT_ENABLE:
payment_handlers.append(process_callback_pay_wata)
if len(payment_handlers) == 1:
await callback_query.answer()
@@ -88,6 +88,9 @@ async def handle_pay(callback_query: CallbackQuery, state: FSMContext, session:
builder.row(InlineKeyboardButton(text=YOOKASSA, callback_data="pay_yookassa"))
if YOOMONEY_ENABLE:
builder.row(InlineKeyboardButton(text=YOOMONEY, callback_data="pay_yoomoney"))
if KASSAI_ENABLE:
builder.row(InlineKeyboardButton(text=KASSAI_CARDS, callback_data="pay_kassai_cards"))
builder.row(InlineKeyboardButton(text=KASSAI_SBP, callback_data="pay_kassai_sbp"))
if CRYPTO_BOT_ENABLE:
builder.row(InlineKeyboardButton(text=CRYPTOBOT, callback_data="pay_cryptobot"))
if STARS_ENABLE:
@@ -96,9 +99,6 @@ async def handle_pay(callback_query: CallbackQuery, state: FSMContext, session:
builder.row(InlineKeyboardButton(text=ROBOKASSA, callback_data="pay_robokassa"))
if FREEKASSA_ENABLE:
builder.row(InlineKeyboardButton(text=FREEKASSA, callback_data="pay_freekassa"))
if KASSAI_ENABLE:
builder.row(InlineKeyboardButton(text=KASSAI_CARDS, callback_data="pay_kassai_cards"))
builder.row(InlineKeyboardButton(text=KASSAI_SBP, callback_data="pay_kassai_sbp"))
if WATA_RU_ENABLE:
builder.row(InlineKeyboardButton(text=WATA_RU, callback_data="pay_wata_ru"))
if WATA_SBP_ENABLE:
+11 -45
View File
@@ -226,7 +226,6 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext):
async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
parts = callback_query.data.split("|")
method_name = parts[1]
# Теперь сумма приходит прямо в parts[2]
amount_str = parts[2]
method = next((m for m in KASSAI_PAYMENT_METHODS if m["name"] == method_name), None)
@@ -276,101 +275,72 @@ async def generate_kassai_payment_link(amount: int, tg_id: int, method: dict) ->
"""
Создание заказа в KassaAI и получение ссылки на оплату
"""
# Генерируем уникальный nonce (timestamp в секундах)
nonce = int(time.time())
# Генерируем уникальный paymentId для отслеживания заказа
unique_payment_id = f"{nonce}_{tg_id}"
# Формируем URL с query параметрами (включая paymentId)
url = f"https://api.fk.life/v1/orders/create?paymentId={unique_payment_id}"
headers = {
"Content-Type": "application/json",
}
# Формируем email клиента (тг_id@домен)
client_email = f"{tg_id}@{KASSAI_DOMAIN}"
client_ip = KASSAI_IP
# Используем IP сервера из конфигурации
client_ip = KASSAI_IP # IP сервера
# Подготавливаем ТОЛЬКО обязательные данные для подписи и запроса
# Согласно документации: shopId, nonce, i, email, ip, amount, currency
data_for_signature = {
"shopId": KASSAI_SHOP_ID,
"nonce": nonce,
"i": method["method"],
"email": client_email,
"ip": client_ip,
"amount": int(amount), # Целое число
"amount": int(amount),
"currency": "RUB"
}
# Формируем подпись согласно документации PHP примера:
# ksort($data); hash_hmac('sha256', implode('|', $data), $api_key)
sorted_keys = sorted(data_for_signature.keys())
values = [str(data_for_signature[key]) for key in sorted_keys]
sign_string = "|".join(values)
# Отладочная информация
logger.info(f"KassaAI signature debug (correct algorithm):")
logger.info(f"Sorted keys: {sorted_keys}")
logger.info(f"Values: {values}")
logger.info(f"Sign string: {sign_string}")
logger.info(f"API key length: {len(KASSAI_API_KEY)}")
logger.info(f"Sending to URL: {url}")
# HMAC SHA256 как в PHP примере
signature = hmac.new(
KASSAI_API_KEY.encode('utf-8'),
sign_string.encode('utf-8'),
hashlib.sha256
).hexdigest()
logger.info(f"Generated signature: {signature}")
# Данные для отправки (только обязательные поля + подпись)
data = data_for_signature.copy()
data["signature"] = signature
logger.info(f"Sending data: {data}")
try:
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, json=data, timeout=60) as resp:
if resp.status == 200:
try:
resp_json = await resp.json()
logger.info(f"KassaAI API response: {resp_json}")
# API возвращает успешный ответ со ссылкой в поле location
if resp_json.get("type") == "success":
payment_url = resp_json.get("location")
if payment_url:
logger.info(f"KassaAI payment URL created: {payment_url}")
logger.info(f"KassaAI payment URL created for user {tg_id}")
return payment_url
else:
logger.error(f"KassaAI: Нет поля location в успешном ответе: {resp_json}")
logger.error(f"KassaAI: No location in response: {resp_json}")
return "https://fk.life/"
else:
logger.error(f"KassaAI: Неуспешный ответ: {resp_json}")
logger.error(f"KassaAI: Unsuccessful response: {resp_json}")
return "https://fk.life/"
except Exception as e:
logger.error(f"KassaAI: Ошибка при разборе JSON ответа: {e}")
logger.error(f"KassaAI: Error parsing JSON response: {e}")
text = await resp.text()
logger.error(f"KassaAI: Содержимое ответа: {text}")
logger.error(f"KassaAI: Response content: {text}")
return "https://fk.life/"
else:
try:
error_json = await resp.json()
logger.error(f"Ошибка KassaAI API: статус={resp.status}, ответ={error_json}")
logger.error(f"KassaAI API error: status={resp.status}, response={error_json}")
except Exception:
text = await resp.text()
logger.error(f"Ошибка KassaAI API: статус={resp.status}, не-JSON ответ: {text}")
logger.error(f"KassaAI API error: status={resp.status}, non-JSON response: {text}")
return "https://fk.life/"
except Exception as e:
logger.error(f"Ошибка при создании заказа KassaAI: {e}")
logger.error(f"Error creating KassaAI order: {e}")
return "https://fk.life/"
@@ -380,19 +350,15 @@ def verify_kassai_signature(data: dict, signature: str) -> bool:
Формат: MERCHANT_ID:AMOUNT:SECRET_KEY2:MERCHANT_ORDER_ID
"""
try:
# Формируем строку для подписи согласно документации:
# MERCHANT_ID:AMOUNT:SECRET_KEY2:MERCHANT_ORDER_ID
sign_string = (
f"{KASSAI_SHOP_ID}:"
f"{data.get('AMOUNT', '')}:"
f"{KASSAI_SECRET_KEY}:" # Используем SECRET_KEY2 для вебхуков
f"{KASSAI_SECRET_KEY}:"
f"{data.get('MERCHANT_ORDER_ID', '')}"
)
# Вычисляем MD5 хеш
expected_signature = hashlib.md5(sign_string.encode('utf-8')).hexdigest()
# Сравниваем подписи (регистронезависимо)
result = signature.upper() == expected_signature.upper()
if not result:
+13 -53
View File
@@ -7,128 +7,88 @@ from handlers.payments.kassai import verify_kassai_signature
from config import KASSAI_SECRET_KEY, KASSAI_SHOP_ID
from logger import logger
# Хранилище обработанных платежей для предотвращения дублирования
processed_payments = set()
async def kassai_payment_webhook(request: web.Request):
"""
Обработчик вебхука от KassaAI для подтверждения оплаты
Формат: MERCHANT_ID=123&AMOUNT=100&intid=123456&MERCHANT_ORDER_ID=test_order&SIGN=...
"""
try:
# Получаем данные из POST запроса (form-data)
data = await request.post()
data_dict = dict(data)
# Логируем входящий запрос
logger.info(f"KassaAI webhook received: {data_dict}")
logger.info(f"KassaAI webhook received from {request.remote}")
# Получаем подпись из параметров
signature = data_dict.get("SIGN")
if not signature:
logger.error("KassaAI: Нет подписи SIGN в запросе!")
logger.error("KassaAI: Missing SIGN in request")
return web.Response(status=400, text="Signature missing")
# Проверяем подпись согласно документации KassaAI
if not verify_kassai_webhook_signature(data_dict, signature):
logger.error("KassaAI: Подпись не прошла проверку!")
logger.error("KassaAI: Invalid signature")
return web.Response(status=400, text="Invalid signature")
# Логируем все важные параметры
logger.info(
f"KassaAI webhook details: "
f"MERCHANT_ID={data_dict.get('MERCHANT_ID')}, "
f"AMOUNT={data_dict.get('AMOUNT')}, "
f"intid={data_dict.get('intid')}, "
f"MERCHANT_ORDER_ID={data_dict.get('MERCHANT_ORDER_ID')}, "
f"P_EMAIL={data_dict.get('P_EMAIL')}, "
f"CUR_ID={data_dict.get('CUR_ID')}, "
f"SIGN={data_dict.get('SIGN')}"
)
# Получаем необходимые данные
merchant_order_id = data_dict.get("MERCHANT_ORDER_ID")
amount = data_dict.get("AMOUNT")
p_email = data_dict.get("P_EMAIL")
if not amount:
logger.error(f"KassaAI: отсутствует AMOUNT: {data_dict}")
logger.error(f"KassaAI: Missing AMOUNT")
return web.Response(status=400, text="Missing required fields")
# Проверяем уникальность платежа по MERCHANT_ORDER_ID (если есть)
if merchant_order_id and merchant_order_id in processed_payments:
logger.warning(f"KassaAI: платеж MERCHANT_ORDER_ID={merchant_order_id} уже был обработан. Игнорируем дубликат.")
return web.Response(status=200, text="YES") # Возвращаем успех чтобы не было повторных попыток
logger.warning(f"KassaAI: Duplicate payment {merchant_order_id}")
return web.Response(status=200, text="YES")
try:
# Извлекаем tg_id из email (формат: tg_id@domain)
if p_email and "@" in p_email:
tg_id = int(p_email.split("@")[0])
logger.info(f"KassaAI: извлечен tg_id={tg_id} из email={p_email}")
else:
logger.error(f"KassaAI: некорректный email для извлечения tg_id: {p_email}")
logger.error(f"KassaAI: Invalid email format: {p_email}")
return web.Response(status=400, text="Invalid email format")
amount_float = float(amount)
logger.info(f"KassaAI: обрабатываем платеж MERCHANT_ORDER_ID={merchant_order_id}, tg_id={tg_id}, amount={amount_float}")
except (ValueError, TypeError) as e:
logger.error(f"KassaAI: некорректные данные tg_id или amount: {e}")
logger.error(f"KassaAI: Invalid data format: {e}")
return web.Response(status=400, text="Invalid data format")
# Обновляем баланс пользователя
async with async_session_maker() as session:
await update_balance(session, tg_id, amount_float)
await send_payment_success_notification(tg_id, amount_float, session)
await add_payment(session, tg_id, amount_float, "kassai")
# Добавляем MERCHANT_ORDER_ID в список обработанных платежей (если есть)
if merchant_order_id:
processed_payments.add(merchant_order_id)
logger.info(f"✅ KassaAI: баланс пополнен для пользователя {tg_id} на {amount_float}, MERCHANT_ORDER_ID={merchant_order_id}")
# Возвращаем ответ согласно документации KassaAI
logger.info(f"✅ KassaAI: Payment processed for user {tg_id}, amount {amount_float}")
return web.Response(status=200, text="YES")
except Exception as e:
logger.error(f"Ошибка обработки KassaAI webhook: {e}")
logger.error(f"KassaAI webhook error: {e}")
return web.Response(status=500, text="Internal server error")
def verify_kassai_webhook_signature(data: dict, signature: str) -> bool:
"""
Проверка подписи вебхука KassaAI согласно документации FreeKassa
Формат: MERCHANT_ID:AMOUNT:SECRET_KEY2:MERCHANT_ORDER_ID
"""
try:
# Формируем строку для подписи согласно документации FreeKassa:
# MERCHANT_ID:AMOUNT:SECRET_KEY2:MERCHANT_ORDER_ID
sign_string = (
f"{data.get('MERCHANT_ID', '')}:"
f"{data.get('AMOUNT', '')}:"
f"{KASSAI_SECRET_KEY}:" # Используем SECRET_KEY2 для вебхуков
f"{KASSAI_SECRET_KEY}:"
f"{data.get('MERCHANT_ORDER_ID', '')}"
)
# Вычисляем MD5 хеш
expected_signature = hashlib.md5(sign_string.encode('utf-8')).hexdigest()
# Сравниваем подписи (регистронезависимо)
result = signature.upper() == expected_signature.upper()
if not result:
logger.error(f"KassaAI webhook signature mismatch:")
logger.error(f"Expected: {expected_signature}")
logger.error(f"Received: {signature}")
logger.error(f"Sign string: {sign_string}")
logger.error(f"MERCHANT_ID: {data.get('MERCHANT_ID')}")
logger.error(f"AMOUNT: {data.get('AMOUNT')}")
logger.error(f"MERCHANT_ORDER_ID: {data.get('MERCHANT_ORDER_ID')}")
logger.error(f"KassaAI webhook signature mismatch")
return result
except Exception as e:
logger.error(f"Ошибка проверки подписи KassaAI webhook: {e}")
logger.error(f"KassaAI signature verification error: {e}")
return False