Fix CryptoPay import
This commit is contained in:
+11
-7
@@ -19,24 +19,30 @@ TERMS_OF_SERVICE_URL=https://example.com/tos
|
|||||||
SUBSCRIPTION_MINI_APP_URL=
|
SUBSCRIPTION_MINI_APP_URL=
|
||||||
START_COMMAND_DESCRIPTION=
|
START_COMMAND_DESCRIPTION=
|
||||||
|
|
||||||
|
# Webhook Base URL (used for Telegram and payment providers)
|
||||||
|
WEBHOOK_BASE_URL=https://webhooks.yourdomain.tld
|
||||||
|
|
||||||
# YooKassa Payment Gateway Configuration
|
# YooKassa Payment Gateway Configuration
|
||||||
YOOKASSA_SHOP_ID=your_shop_id
|
YOOKASSA_SHOP_ID=your_shop_id
|
||||||
YOOKASSA_SECRET_KEY=your_secret_key
|
YOOKASSA_SECRET_KEY=your_secret_key
|
||||||
YOOKASSA_WEBHOOK_BASE_URL=https://webhooks.yourdomain.tld
|
|
||||||
YOOKASSA_RETURN_URL=https://t.me/your_bot
|
YOOKASSA_RETURN_URL=https://t.me/your_bot
|
||||||
YOOKASSA_DEFAULT_RECEIPT_EMAIL=your_email@example.com
|
YOOKASSA_DEFAULT_RECEIPT_EMAIL=your_email@example.com
|
||||||
YOOKASSA_VAT_CODE=1
|
YOOKASSA_VAT_CODE=1
|
||||||
YOOKASSA_PAYMENT_MODE=full_prepayment
|
YOOKASSA_PAYMENT_MODE=full_prepayment
|
||||||
YOOKASSA_PAYMENT_SUBJECT=payment
|
YOOKASSA_PAYMENT_SUBJECT=payment
|
||||||
|
|
||||||
# Telegram Webhook Configuration (Optional)
|
# CryptoBot Payment Gateway Configuration
|
||||||
# If unset, bot will use polling while YooKassa uses webhooks.
|
CRYPTOPAY_TOKEN=
|
||||||
TELEGRAM_WEBHOOK_BASE_URL=https://webhooks.yourdomain.tld
|
CRYPTOPAY_NETWORK=mainnet
|
||||||
|
CRYPTOPAY_ASSET=TON
|
||||||
|
# API key for verifying Tribute webhook signatures
|
||||||
|
TRIBUTE_API_KEY=
|
||||||
|
|
||||||
# Payment Methods
|
# Payment Method Toggles
|
||||||
YOOKASSA_ENABLED=True
|
YOOKASSA_ENABLED=True
|
||||||
STARS_ENABLED=True
|
STARS_ENABLED=True
|
||||||
TRIBUTE_ENABLED=True
|
TRIBUTE_ENABLED=True
|
||||||
|
CRYPTOPAY_ENABLED=True
|
||||||
|
|
||||||
# Subscription Options
|
# Subscription Options
|
||||||
1_MONTH_ENABLED=True
|
1_MONTH_ENABLED=True
|
||||||
@@ -58,8 +64,6 @@ TRIBUTE_LINK_6_MONTHS=
|
|||||||
RUB_PRICE_12_MONTHS=900
|
RUB_PRICE_12_MONTHS=900
|
||||||
STARS_PRICE_12_MONTHS=0
|
STARS_PRICE_12_MONTHS=0
|
||||||
TRIBUTE_LINK_12_MONTHS=
|
TRIBUTE_LINK_12_MONTHS=
|
||||||
# API key for verifying Tribute webhook signatures
|
|
||||||
TRIBUTE_API_KEY=
|
|
||||||
|
|
||||||
# Subscription Notifications
|
# Subscription Notifications
|
||||||
SUBSCRIPTION_NOTIFICATIONS_ENABLED=True
|
SUBSCRIPTION_NOTIFICATIONS_ENABLED=True
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ This Telegram bot is designed to automate the sale and management of subscriptio
|
|||||||
* **Subscription Management:**
|
* **Subscription Management:**
|
||||||
* Handles subscription purchases for various periods (1, 3, 6, 12 months).
|
* Handles subscription purchases for various periods (1, 3, 6, 12 months).
|
||||||
* Integrates with **YooKassa** for payment processing, including fiscal receipt data.
|
* Integrates with **YooKassa** for payment processing, including fiscal receipt data.
|
||||||
|
* Supports **Crypto Pay** for cryptocurrency payments.
|
||||||
* Automatic subscription activation/extension upon successful payment.
|
* Automatic subscription activation/extension upon successful payment.
|
||||||
* Link and syncs users with a **Remnawave panel** account, primarily matching by Telegram ID.
|
* Link and syncs users with a **Remnawave panel** account, primarily matching by Telegram ID.
|
||||||
* Updates user status, expiration dates, traffic limits, and inbounds on the Remnawave panel.
|
* Updates user status, expiration dates, traffic limits, and inbounds on the Remnawave panel.
|
||||||
@@ -88,14 +89,14 @@ This Telegram bot is designed to automate the sale and management of subscriptio
|
|||||||
* **YooKassa Settings:**
|
* **YooKassa Settings:**
|
||||||
* `YOOKASSA_SHOP_ID`: Your shop ID from YooKassa.
|
* `YOOKASSA_SHOP_ID`: Your shop ID from YooKassa.
|
||||||
* `YOOKASSA_SECRET_KEY`: Your secret key from YooKassa.
|
* `YOOKASSA_SECRET_KEY`: Your secret key from YooKassa.
|
||||||
* `YOOKASSA_WEBHOOK_BASE_URL`: Your publicly accessible HTTPS base URL where the bot will listen for YooKassa webhooks (e.g., `https://your.domain.com`). The full path will be `{YOOKASSA_WEBHOOK_BASE_URL}/webhook/yookassa`.
|
* `WEBHOOK_BASE_URL`: Base URL for all webhooks (Telegram, YooKassa, Crypto Pay). Example: `https://webhooks.yourdomain.com`.
|
||||||
* `YOOKASSA_RETURN_URL`: (Optional) URL user is redirected to after payment, often `https://t.me/your_bot_username`.
|
* `YOOKASSA_RETURN_URL`: (Optional) URL user is redirected to after payment, often `https://t.me/your_bot_username`.
|
||||||
* `YOOKASSA_DEFAULT_RECEIPT_EMAIL`: **Important for 54-FZ (Russian fiscalization).** A default email for sending fiscal receipts.
|
* `YOOKASSA_DEFAULT_RECEIPT_EMAIL`: **Important for 54-FZ (Russian fiscalization).** A default email for sending fiscal receipts.
|
||||||
* `YOOKASSA_VAT_CODE`: VAT code for items in receipt (e.g., `1` for "No VAT". Consult YooKassa documentation and tax advisor).
|
* `YOOKASSA_VAT_CODE`: VAT code for items in receipt (e.g., `1` for "No VAT". Consult YooKassa documentation and tax advisor).
|
||||||
* `YOOKASSA_PAYMENT_MODE`: e.g., `full_prepayment`.
|
* `YOOKASSA_PAYMENT_MODE`: e.g., `full_prepayment`.
|
||||||
* `YOOKASSA_PAYMENT_SUBJECT`: e.g., `service`.
|
* `YOOKASSA_PAYMENT_SUBJECT`: e.g., `service`.
|
||||||
* `TELEGRAM_WEBHOOK_BASE_URL`: (Optional) If you want Telegram updates via webhook. Can be the same as `YOOKASSA_WEBHOOK_BASE_URL`. If not set, the bot will use polling for Telegram updates.
|
* **Crypto Pay Settings:** `CRYPTOPAY_TOKEN`, `CRYPTOPAY_NETWORK` (`mainnet` or `testnet`), `CRYPTOPAY_ASSET` (e.g., `TON`). Enable with `CRYPTOPAY_ENABLED`.
|
||||||
* **Payment Method Toggles:** `YOOKASSA_ENABLED`, `STARS_ENABLED`, `TRIBUTE_ENABLED`.
|
* **Payment Method Toggles:** `YOOKASSA_ENABLED`, `STARS_ENABLED`, `TRIBUTE_ENABLED`, `CRYPTOPAY_ENABLED`.
|
||||||
* **Subscription Options:** For each duration you can use variables like
|
* **Subscription Options:** For each duration you can use variables like
|
||||||
`1_MONTH_ENABLED`, `RUB_PRICE_1_MONTH`, `STARS_PRICE_1_MONTH`, `TRIBUTE_LINK_1_MONTH`
|
`1_MONTH_ENABLED`, `RUB_PRICE_1_MONTH`, `STARS_PRICE_1_MONTH`, `TRIBUTE_LINK_1_MONTH`
|
||||||
(and corresponding variables for `3_MONTHS`, `6_MONTHS`, `12_MONTHS`).
|
(and corresponding variables for `3_MONTHS`, `6_MONTHS`, `12_MONTHS`).
|
||||||
@@ -120,9 +121,11 @@ This Telegram bot is designed to automate the sale and management of subscriptio
|
|||||||
|
|
||||||
5. **Webhook Setup (Important if using webhooks):**
|
5. **Webhook Setup (Important if using webhooks):**
|
||||||
* **Reverse Proxy (Nginx, Caddy, etc.):** You need a reverse proxy to handle incoming HTTPS traffic, manage SSL certificates (e.g., from Let's Encrypt), and forward requests to your bot's container.
|
* **Reverse Proxy (Nginx, Caddy, etc.):** You need a reverse proxy to handle incoming HTTPS traffic, manage SSL certificates (e.g., from Let's Encrypt), and forward requests to your bot's container.
|
||||||
* Forward requests for `https://{YOOKASSA_WEBHOOK_BASE_URL_domain}/webhook/yookassa` to `http://remnawave-tg-shop:{WEB_SERVER_PORT}/webhook/yookassa` (where `remnawave-tg-shop` is the service name in `docker-compose.yml`).
|
* Forward requests for `https://{WEBHOOK_BASE_URL_domain}/webhook/yookassa` to `http://remnawave-tg-shop:{WEB_SERVER_PORT}/webhook/yookassa` (where `remnawave-tg-shop` is the service name in `docker-compose.yml`).
|
||||||
* If using Telegram webhooks, forward requests for `https://{TELEGRAM_WEBHOOK_BASE_URL_domain}/<YOUR_BOT_TOKEN>` to `http://remnawave-tg-shop:{WEB_SERVER_PORT}/<YOUR_BOT_TOKEN>`.
|
* Forward requests for `https://{WEBHOOK_BASE_URL_domain}/webhook/cryptopay` to `http://remnawave-tg-shop:{WEB_SERVER_PORT}/webhook/cryptopay`.
|
||||||
* **Telegram Webhook Registration:** The bot attempts to set its Telegram webhook URL on startup if `TELEGRAM_WEBHOOK_BASE_URL` is configured in `.env`. Check the bot logs to confirm if this was successful. You can also manually check using the Telegram Bot API method `getWebhookInfo`.
|
* Forward requests for `https://{WEBHOOK_BASE_URL_domain}/webhook/tribute` to `http://remnawave-tg-shop:{WEB_SERVER_PORT}/webhook/tribute`.
|
||||||
|
* If using Telegram webhooks, forward requests for `https://{WEBHOOK_BASE_URL_domain}/<YOUR_BOT_TOKEN>` to `http://remnawave-tg-shop:{WEB_SERVER_PORT}/<YOUR_BOT_TOKEN>`.
|
||||||
|
* **Telegram Webhook Registration:** The bot attempts to set its Telegram webhook URL on startup if `WEBHOOK_BASE_URL` is configured in `.env`. Check the bot logs to confirm if this was successful. You can also manually check using the Telegram Bot API method `getWebhookInfo`.
|
||||||
|
|
||||||
6. **Database:**
|
6. **Database:**
|
||||||
* A PostgreSQL database will be created in the docker container. The schema is initialized automatically on the first run if the database doesn't exist.
|
* A PostgreSQL database will be created in the docker container. The schema is initialized automatically on the first run if the database doesn't exist.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from bot.keyboards.inline.user_keyboards import (
|
|||||||
get_payment_url_keyboard, get_back_to_main_menu_markup)
|
get_payment_url_keyboard, get_back_to_main_menu_markup)
|
||||||
from bot.services.yookassa_service import YooKassaService
|
from bot.services.yookassa_service import YooKassaService
|
||||||
from bot.services.stars_service import StarsService
|
from bot.services.stars_service import StarsService
|
||||||
|
from bot.services.crypto_pay_service import CryptoPayService
|
||||||
from bot.services.subscription_service import SubscriptionService
|
from bot.services.subscription_service import SubscriptionService
|
||||||
from bot.services.panel_api_service import PanelApiService
|
from bot.services.panel_api_service import PanelApiService
|
||||||
from bot.services.referral_service import ReferralService
|
from bot.services.referral_service import ReferralService
|
||||||
@@ -293,6 +294,49 @@ async def pay_yk_callback_handler(
|
|||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("pay_crypto:"))
|
||||||
|
async def pay_crypto_callback_handler(
|
||||||
|
callback: types.CallbackQuery, settings: Settings, i18n_data: dict,
|
||||||
|
cryptopay_service: CryptoPayService, session: AsyncSession):
|
||||||
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||||
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||||
|
get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
|
||||||
|
|
||||||
|
if not i18n or not callback.message:
|
||||||
|
await callback.answer(get_text("error_occurred_try_again"), show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not cryptopay_service or not cryptopay_service.configured:
|
||||||
|
await callback.message.edit_text(get_text("payment_service_unavailable"))
|
||||||
|
await callback.answer(get_text("payment_service_unavailable_alert"), show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
_, data_payload = callback.data.split(":", 1)
|
||||||
|
months_str, amount_str = data_payload.split(":")
|
||||||
|
months = int(months_str)
|
||||||
|
amount_val = float(amount_str)
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
logging.error(f"Invalid pay_crypto data in callback: {callback.data}")
|
||||||
|
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
user_id = callback.from_user.id
|
||||||
|
description = get_text("payment_description_subscription", months=months)
|
||||||
|
|
||||||
|
invoice_url = await cryptopay_service.create_invoice(
|
||||||
|
session, user_id, months, amount_val, description)
|
||||||
|
if invoice_url:
|
||||||
|
await callback.message.edit_text(
|
||||||
|
get_text("payment_link_message", months=months),
|
||||||
|
reply_markup=get_payment_url_keyboard(invoice_url, current_lang, i18n),
|
||||||
|
disable_web_page_preview=False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await callback.message.edit_text(get_text("error_payment_gateway"))
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "main_action:subscribe")
|
@router.callback_query(F.data == "main_action:subscribe")
|
||||||
async def reshow_subscription_options_callback(callback: types.CallbackQuery,
|
async def reshow_subscription_options_callback(callback: types.CallbackQuery,
|
||||||
i18n_data: dict,
|
i18n_data: dict,
|
||||||
|
|||||||
@@ -134,6 +134,9 @@ def get_payment_method_keyboard(months: int, price: float,
|
|||||||
if settings.YOOKASSA_ENABLED:
|
if settings.YOOKASSA_ENABLED:
|
||||||
builder.button(text=_("pay_with_yookassa_button"),
|
builder.button(text=_("pay_with_yookassa_button"),
|
||||||
callback_data=f"pay_yk:{months}:{price}")
|
callback_data=f"pay_yk:{months}:{price}")
|
||||||
|
if settings.CRYPTOPAY_ENABLED:
|
||||||
|
builder.button(text=_("pay_with_cryptopay_button"),
|
||||||
|
callback_data=f"pay_crypto:{months}:{price}")
|
||||||
builder.button(text=_(key="cancel_button"),
|
builder.button(text=_(key="cancel_button"),
|
||||||
callback_data="main_action:subscribe")
|
callback_data="main_action:subscribe")
|
||||||
builder.adjust(1)
|
builder.adjust(1)
|
||||||
|
|||||||
+24
-6
@@ -38,6 +38,7 @@ from bot.services.referral_service import ReferralService
|
|||||||
from bot.services.promo_code_service import PromoCodeService
|
from bot.services.promo_code_service import PromoCodeService
|
||||||
from bot.services.stars_service import StarsService
|
from bot.services.stars_service import StarsService
|
||||||
from bot.services.tribute_service import TributeService, tribute_webhook_route
|
from bot.services.tribute_service import TributeService, tribute_webhook_route
|
||||||
|
from bot.services.crypto_pay_service import CryptoPayService, cryptopay_webhook_route
|
||||||
|
|
||||||
from bot.handlers.user import payment as user_payment_webhook_module
|
from bot.handlers.user import payment as user_payment_webhook_module
|
||||||
|
|
||||||
@@ -101,7 +102,7 @@ async def on_startup_configured(dispatcher: Dispatcher):
|
|||||||
logging.info("STARTUP: on_startup_configured executing...")
|
logging.info("STARTUP: on_startup_configured executing...")
|
||||||
|
|
||||||
|
|
||||||
telegram_webhook_url_to_set = getattr(settings, "TELEGRAM_WEBHOOK_BASE_URL", None)
|
telegram_webhook_url_to_set = settings.WEBHOOK_BASE_URL
|
||||||
if telegram_webhook_url_to_set:
|
if telegram_webhook_url_to_set:
|
||||||
full_telegram_webhook_url = (
|
full_telegram_webhook_url = (
|
||||||
f"{str(telegram_webhook_url_to_set).rstrip('/')}/{settings.BOT_TOKEN}"
|
f"{str(telegram_webhook_url_to_set).rstrip('/')}/{settings.BOT_TOKEN}"
|
||||||
@@ -152,7 +153,7 @@ async def on_startup_configured(dispatcher: Dispatcher):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logging.info(
|
logging.info(
|
||||||
"STARTUP: TELEGRAM_WEBHOOK_BASE_URL not set in environment. Attempting to delete any existing webhook (running in polling mode)."
|
"STARTUP: WEBHOOK_BASE_URL not set in environment. Running in polling mode and clearing any existing webhook."
|
||||||
)
|
)
|
||||||
await bot.delete_webhook(drop_pending_updates=True)
|
await bot.delete_webhook(drop_pending_updates=True)
|
||||||
|
|
||||||
@@ -265,6 +266,16 @@ async def run_bot(settings_param: Settings):
|
|||||||
stars_service = StarsService(
|
stars_service = StarsService(
|
||||||
bot, settings_param, i18n_instance, subscription_service, referral_service
|
bot, settings_param, i18n_instance, subscription_service, referral_service
|
||||||
)
|
)
|
||||||
|
cryptopay_service = CryptoPayService(
|
||||||
|
settings_param.CRYPTOPAY_TOKEN,
|
||||||
|
settings_param.CRYPTOPAY_NETWORK,
|
||||||
|
bot,
|
||||||
|
settings_param,
|
||||||
|
i18n_instance,
|
||||||
|
local_async_session_factory,
|
||||||
|
subscription_service,
|
||||||
|
referral_service,
|
||||||
|
)
|
||||||
tribute_service = TributeService(
|
tribute_service = TributeService(
|
||||||
bot,
|
bot,
|
||||||
settings_param,
|
settings_param,
|
||||||
@@ -288,6 +299,7 @@ async def run_bot(settings_param: Settings):
|
|||||||
dp["referral_service"] = referral_service
|
dp["referral_service"] = referral_service
|
||||||
dp["promo_code_service"] = promo_code_service
|
dp["promo_code_service"] = promo_code_service
|
||||||
dp["stars_service"] = stars_service
|
dp["stars_service"] = stars_service
|
||||||
|
dp["cryptopay_service"] = cryptopay_service
|
||||||
dp["tribute_service"] = tribute_service
|
dp["tribute_service"] = tribute_service
|
||||||
dp["panel_webhook_service"] = panel_webhook_service
|
dp["panel_webhook_service"] = panel_webhook_service
|
||||||
dp["async_session_factory"] = local_async_session_factory
|
dp["async_session_factory"] = local_async_session_factory
|
||||||
@@ -306,8 +318,8 @@ async def run_bot(settings_param: Settings):
|
|||||||
|
|
||||||
await register_all_routers(dp, settings_param)
|
await register_all_routers(dp, settings_param)
|
||||||
|
|
||||||
tg_webhook_base = getattr(settings_param, "TELEGRAM_WEBHOOK_BASE_URL", None)
|
tg_webhook_base = settings_param.WEBHOOK_BASE_URL
|
||||||
yk_webhook_base = getattr(settings_param, "YOOKASSA_WEBHOOK_BASE_URL", None)
|
yk_webhook_base = settings_param.WEBHOOK_BASE_URL
|
||||||
|
|
||||||
should_run_aiohttp_server = bool(tg_webhook_base) or (
|
should_run_aiohttp_server = bool(tg_webhook_base) or (
|
||||||
bool(yk_webhook_base) and bool(settings_param.yookassa_webhook_path)
|
bool(yk_webhook_base) and bool(settings_param.yookassa_webhook_path)
|
||||||
@@ -318,10 +330,10 @@ async def run_bot(settings_param: Settings):
|
|||||||
|
|
||||||
logging.info(f"--- Bot Run Mode Decision ---")
|
logging.info(f"--- Bot Run Mode Decision ---")
|
||||||
logging.info(
|
logging.info(
|
||||||
f"Configured TELEGRAM_WEBHOOK_BASE_URL: '{tg_webhook_base}' -> Telegram Webhook Mode: {telegram_uses_webhook_mode}"
|
f"Configured WEBHOOK_BASE_URL: '{tg_webhook_base}' -> Telegram Webhook Mode: {telegram_uses_webhook_mode}"
|
||||||
)
|
)
|
||||||
logging.info(
|
logging.info(
|
||||||
f"Configured YOOKASSA_WEBHOOK_BASE_URL: '{yk_webhook_base}' & Path: '{settings_param.yookassa_webhook_path}'"
|
f"YooKassa webhook path: '{settings_param.yookassa_webhook_path}'"
|
||||||
)
|
)
|
||||||
logging.info(f"Decision: Run AIOHTTP server: {should_run_aiohttp_server}")
|
logging.info(f"Decision: Run AIOHTTP server: {should_run_aiohttp_server}")
|
||||||
logging.info(f"Decision: Run Telegram Polling: {run_telegram_polling}")
|
logging.info(f"Decision: Run Telegram Polling: {run_telegram_polling}")
|
||||||
@@ -343,6 +355,7 @@ async def run_bot(settings_param: Settings):
|
|||||||
app["referral_service"] = referral_service
|
app["referral_service"] = referral_service
|
||||||
app["panel_service"] = panel_service
|
app["panel_service"] = panel_service
|
||||||
app["stars_service"] = stars_service
|
app["stars_service"] = stars_service
|
||||||
|
app["cryptopay_service"] = cryptopay_service
|
||||||
app["tribute_service"] = tribute_service
|
app["tribute_service"] = tribute_service
|
||||||
app["panel_webhook_service"] = panel_webhook_service
|
app["panel_webhook_service"] = panel_webhook_service
|
||||||
|
|
||||||
@@ -380,6 +393,11 @@ async def run_bot(settings_param: Settings):
|
|||||||
app.router.add_post(tribute_path, tribute_webhook_route)
|
app.router.add_post(tribute_path, tribute_webhook_route)
|
||||||
logging.info(f"Tribute webhook route configured at: [POST] {tribute_path}")
|
logging.info(f"Tribute webhook route configured at: [POST] {tribute_path}")
|
||||||
|
|
||||||
|
cp_path = settings_param.cryptopay_webhook_path
|
||||||
|
if cp_path.startswith("/"):
|
||||||
|
app.router.add_post(cp_path, cryptopay_webhook_route)
|
||||||
|
logging.info(f"CryptoPay webhook route configured at: [POST] {cp_path}")
|
||||||
|
|
||||||
panel_path = settings_param.panel_webhook_path
|
panel_path = settings_param.panel_webhook_path
|
||||||
if panel_path.startswith("/"):
|
if panel_path.startswith("/"):
|
||||||
app.router.add_post(panel_path, panel_webhook_route)
|
app.router.add_post(panel_path, panel_webhook_route)
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import logging
|
||||||
|
import json
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from aiogram import Bot
|
||||||
|
from aiohttp import web
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from aiocryptopay import AioCryptoPay, Networks
|
||||||
|
from aiocryptopay.models.update import Update
|
||||||
|
|
||||||
|
from config.settings import Settings
|
||||||
|
from bot.middlewares.i18n import JsonI18n
|
||||||
|
from bot.services.subscription_service import SubscriptionService
|
||||||
|
from bot.services.referral_service import ReferralService
|
||||||
|
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
|
||||||
|
from bot.services.notification_service import notify_admin_new_payment
|
||||||
|
from db.dal import payment_dal, user_dal
|
||||||
|
|
||||||
|
|
||||||
|
class CryptoPayService:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
token: Optional[str],
|
||||||
|
network: str,
|
||||||
|
bot: Bot,
|
||||||
|
settings: Settings,
|
||||||
|
i18n: JsonI18n,
|
||||||
|
async_session_factory: sessionmaker,
|
||||||
|
subscription_service: SubscriptionService,
|
||||||
|
referral_service: ReferralService,
|
||||||
|
):
|
||||||
|
self.bot = bot
|
||||||
|
self.settings = settings
|
||||||
|
self.i18n = i18n
|
||||||
|
self.async_session_factory = async_session_factory
|
||||||
|
self.subscription_service = subscription_service
|
||||||
|
self.referral_service = referral_service
|
||||||
|
if token:
|
||||||
|
net = Networks.TEST_NET if str(network).lower() == "testnet" else Networks.MAIN_NET
|
||||||
|
self.client = AioCryptoPay(token=token, network=net)
|
||||||
|
self.client.register_pay_handler(self._invoice_paid_handler)
|
||||||
|
self.configured = True
|
||||||
|
else:
|
||||||
|
logging.warning("CryptoPay token not provided. CryptoPay disabled")
|
||||||
|
self.client = None
|
||||||
|
self.configured = False
|
||||||
|
|
||||||
|
async def create_invoice(
|
||||||
|
self,
|
||||||
|
session: AsyncSession,
|
||||||
|
user_id: int,
|
||||||
|
months: int,
|
||||||
|
amount: float,
|
||||||
|
description: str,
|
||||||
|
) -> Optional[str]:
|
||||||
|
if not self.configured or not self.client:
|
||||||
|
logging.error("CryptoPayService not configured")
|
||||||
|
return None
|
||||||
|
|
||||||
|
payment_record = await payment_dal.create_payment_record(
|
||||||
|
session,
|
||||||
|
{
|
||||||
|
"user_id": user_id,
|
||||||
|
"amount": float(amount),
|
||||||
|
"currency": self.settings.CRYPTOPAY_ASSET,
|
||||||
|
"status": "pending_cryptopay",
|
||||||
|
"description": description,
|
||||||
|
"subscription_duration_months": months,
|
||||||
|
"provider": "cryptopay",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
payload = json.dumps({
|
||||||
|
"user_id": str(user_id),
|
||||||
|
"subscription_months": str(months),
|
||||||
|
"payment_db_id": str(payment_record.payment_id),
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
invoice = await self.client.create_invoice(
|
||||||
|
amount=amount,
|
||||||
|
asset=self.settings.CRYPTOPAY_ASSET,
|
||||||
|
description=description,
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
await payment_dal.update_provider_payment_and_status(
|
||||||
|
session,
|
||||||
|
payment_record.payment_id,
|
||||||
|
str(invoice.invoice_id),
|
||||||
|
str(invoice.status),
|
||||||
|
)
|
||||||
|
return invoice.bot_invoice_url
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"CryptoPay invoice creation failed: {e}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _invoice_paid_handler(self, update: Update, app: web.Application):
|
||||||
|
invoice = update.payload
|
||||||
|
if not invoice.payload:
|
||||||
|
logging.warning("CryptoPay webhook without payload")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
meta = json.loads(invoice.payload)
|
||||||
|
user_id = int(meta["user_id"])
|
||||||
|
months = int(meta["subscription_months"])
|
||||||
|
payment_db_id = int(meta["payment_db_id"])
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to parse CryptoPay payload: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
async_session_factory: sessionmaker = app["async_session_factory"]
|
||||||
|
bot: Bot = app["bot"]
|
||||||
|
settings: Settings = app["settings"]
|
||||||
|
i18n: JsonI18n = app["i18n"]
|
||||||
|
subscription_service: SubscriptionService = app["subscription_service"]
|
||||||
|
referral_service: ReferralService = app["referral_service"]
|
||||||
|
|
||||||
|
async with async_session_factory() as session:
|
||||||
|
try:
|
||||||
|
await payment_dal.update_provider_payment_and_status(
|
||||||
|
session,
|
||||||
|
payment_db_id,
|
||||||
|
str(invoice.invoice_id),
|
||||||
|
"succeeded",
|
||||||
|
)
|
||||||
|
activation = await subscription_service.activate_subscription(
|
||||||
|
session,
|
||||||
|
user_id,
|
||||||
|
months,
|
||||||
|
float(invoice.amount),
|
||||||
|
payment_db_id,
|
||||||
|
provider="cryptopay",
|
||||||
|
)
|
||||||
|
referral_bonus = await referral_service.apply_referral_bonuses_for_payment(
|
||||||
|
session, user_id, months
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
await session.rollback()
|
||||||
|
logging.error(f"Failed to process CryptoPay invoice: {e}", exc_info=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||||
|
lang = db_user.language_code if db_user and db_user.language_code else settings.DEFAULT_LANGUAGE
|
||||||
|
_ = lambda k, **kw: i18n.gettext(lang, k, **kw)
|
||||||
|
|
||||||
|
config_link = activation.get("subscription_url") or _("config_link_not_available")
|
||||||
|
final_end = activation.get("end_date")
|
||||||
|
applied_days = 0
|
||||||
|
if referral_bonus and referral_bonus.get("referee_new_end_date"):
|
||||||
|
final_end = referral_bonus["referee_new_end_date"]
|
||||||
|
applied_days = referral_bonus.get("referee_bonus_applied_days", 0)
|
||||||
|
|
||||||
|
if applied_days:
|
||||||
|
inviter_name_display = _("friend_placeholder")
|
||||||
|
if db_user and db_user.referred_by_id:
|
||||||
|
inviter = await user_dal.get_user_by_id(session, db_user.referred_by_id)
|
||||||
|
if inviter and inviter.first_name:
|
||||||
|
inviter_name_display = inviter.first_name
|
||||||
|
elif inviter and inviter.username:
|
||||||
|
inviter_name_display = f"@{inviter.username}"
|
||||||
|
text = _("payment_successful_with_referral_bonus_full",
|
||||||
|
months=months,
|
||||||
|
base_end_date=activation["end_date"].strftime('%Y-%m-%d'),
|
||||||
|
bonus_days=applied_days,
|
||||||
|
final_end_date=final_end.strftime('%Y-%m-%d'),
|
||||||
|
inviter_name=inviter_name_display,
|
||||||
|
config_link=config_link)
|
||||||
|
else:
|
||||||
|
text = _("payment_successful_full",
|
||||||
|
months=months,
|
||||||
|
end_date=final_end.strftime('%Y-%m-%d'),
|
||||||
|
config_link=config_link)
|
||||||
|
|
||||||
|
markup = get_connect_and_main_keyboard(lang, i18n, settings, config_link)
|
||||||
|
try:
|
||||||
|
await bot.send_message(
|
||||||
|
user_id,
|
||||||
|
text,
|
||||||
|
reply_markup=markup,
|
||||||
|
parse_mode="HTML",
|
||||||
|
disable_web_page_preview=True,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to send CryptoPay success message: {e}")
|
||||||
|
|
||||||
|
await notify_admin_new_payment(
|
||||||
|
bot,
|
||||||
|
settings,
|
||||||
|
i18n,
|
||||||
|
user_id,
|
||||||
|
months,
|
||||||
|
float(invoice.amount),
|
||||||
|
currency=invoice.asset or settings.DEFAULT_CURRENCY_SYMBOL,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def webhook_route(self, request: web.Request) -> web.Response:
|
||||||
|
if not self.configured or not self.client:
|
||||||
|
return web.Response(status=503, text="cryptopay_disabled")
|
||||||
|
return await self.client.get_updates(request)
|
||||||
|
|
||||||
|
|
||||||
|
async def cryptopay_webhook_route(request: web.Request) -> web.Response:
|
||||||
|
service: CryptoPayService = request.app["cryptopay_service"]
|
||||||
|
return await service.webhook_route(request)
|
||||||
+28
-9
@@ -26,7 +26,6 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
YOOKASSA_SHOP_ID: Optional[str] = None
|
YOOKASSA_SHOP_ID: Optional[str] = None
|
||||||
YOOKASSA_SECRET_KEY: Optional[str] = None
|
YOOKASSA_SECRET_KEY: Optional[str] = None
|
||||||
YOOKASSA_WEBHOOK_BASE_URL: Optional[str] = None
|
|
||||||
YOOKASSA_RETURN_URL: Optional[str] = None
|
YOOKASSA_RETURN_URL: Optional[str] = None
|
||||||
|
|
||||||
YOOKASSA_DEFAULT_RECEIPT_EMAIL: Optional[str] = Field(default=None)
|
YOOKASSA_DEFAULT_RECEIPT_EMAIL: Optional[str] = Field(default=None)
|
||||||
@@ -34,7 +33,12 @@ class Settings(BaseSettings):
|
|||||||
YOOKASSA_PAYMENT_MODE: str = Field(default="full_prepayment")
|
YOOKASSA_PAYMENT_MODE: str = Field(default="full_prepayment")
|
||||||
YOOKASSA_PAYMENT_SUBJECT: str = Field(default="service")
|
YOOKASSA_PAYMENT_SUBJECT: str = Field(default="service")
|
||||||
|
|
||||||
TELEGRAM_WEBHOOK_BASE_URL: Optional[str] = None
|
WEBHOOK_BASE_URL: Optional[str] = None
|
||||||
|
|
||||||
|
CRYPTOPAY_TOKEN: Optional[str] = None
|
||||||
|
CRYPTOPAY_NETWORK: str = Field(default="mainnet")
|
||||||
|
CRYPTOPAY_ASSET: str = Field(default="TON")
|
||||||
|
CRYPTOPAY_ENABLED: bool = Field(default=True)
|
||||||
|
|
||||||
YOOKASSA_ENABLED: bool = Field(default=True)
|
YOOKASSA_ENABLED: bool = Field(default=True)
|
||||||
STARS_ENABLED: bool = Field(default=True)
|
STARS_ENABLED: bool = Field(default=True)
|
||||||
@@ -169,9 +173,9 @@ class Settings(BaseSettings):
|
|||||||
@computed_field
|
@computed_field
|
||||||
@property
|
@property
|
||||||
def yookassa_full_webhook_url(self) -> Optional[str]:
|
def yookassa_full_webhook_url(self) -> Optional[str]:
|
||||||
if self.YOOKASSA_WEBHOOK_BASE_URL:
|
base = self.WEBHOOK_BASE_URL
|
||||||
|
if base:
|
||||||
return f"{self.YOOKASSA_WEBHOOK_BASE_URL.rstrip('/')}{self.yookassa_webhook_path}"
|
return f"{base.rstrip('/')}{self.yookassa_webhook_path}"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@@ -182,8 +186,9 @@ class Settings(BaseSettings):
|
|||||||
@computed_field
|
@computed_field
|
||||||
@property
|
@property
|
||||||
def tribute_full_webhook_url(self) -> Optional[str]:
|
def tribute_full_webhook_url(self) -> Optional[str]:
|
||||||
if self.YOOKASSA_WEBHOOK_BASE_URL:
|
base = self.WEBHOOK_BASE_URL
|
||||||
return f"{self.YOOKASSA_WEBHOOK_BASE_URL.rstrip('/')}{self.tribute_webhook_path}"
|
if base:
|
||||||
|
return f"{base.rstrip('/')}{self.tribute_webhook_path}"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
@@ -194,8 +199,22 @@ class Settings(BaseSettings):
|
|||||||
@computed_field
|
@computed_field
|
||||||
@property
|
@property
|
||||||
def panel_full_webhook_url(self) -> Optional[str]:
|
def panel_full_webhook_url(self) -> Optional[str]:
|
||||||
if self.YOOKASSA_WEBHOOK_BASE_URL:
|
base = self.WEBHOOK_BASE_URL
|
||||||
return f"{self.YOOKASSA_WEBHOOK_BASE_URL.rstrip('/')}{self.panel_webhook_path}"
|
if base:
|
||||||
|
return f"{base.rstrip('/')}{self.panel_webhook_path}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def cryptopay_webhook_path(self) -> str:
|
||||||
|
return "/webhook/cryptopay"
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@property
|
||||||
|
def cryptopay_full_webhook_url(self) -> Optional[str]:
|
||||||
|
base = self.WEBHOOK_BASE_URL
|
||||||
|
if base:
|
||||||
|
return f"{base.rstrip('/')}{self.cryptopay_webhook_path}"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@computed_field
|
@computed_field
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"choose_payment_method": "Choose payment method:",
|
"choose_payment_method": "Choose payment method:",
|
||||||
"pay_button": "💳 Pay",
|
"pay_button": "💳 Pay",
|
||||||
"pay_with_yookassa_button": "💳 YooKassa",
|
"pay_with_yookassa_button": "💳 YooKassa",
|
||||||
|
"pay_with_cryptopay_button": "💎 CryptoBot",
|
||||||
"pay_with_tribute_button": "❤️ Tribute",
|
"pay_with_tribute_button": "❤️ Tribute",
|
||||||
"pay_with_stars_button": "🌟 Telegram Stars",
|
"pay_with_stars_button": "🌟 Telegram Stars",
|
||||||
"connect_button": "🔗 Connect",
|
"connect_button": "🔗 Connect",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"choose_payment_method": "Выберите способ оплаты:",
|
"choose_payment_method": "Выберите способ оплаты:",
|
||||||
"pay_button": "💳 Оплатить",
|
"pay_button": "💳 Оплатить",
|
||||||
"pay_with_yookassa_button": "💳 ЮKassa",
|
"pay_with_yookassa_button": "💳 ЮKassa",
|
||||||
|
"pay_with_cryptopay_button": "💎 CryptoBot",
|
||||||
"pay_with_tribute_button": "❤️ Tribute",
|
"pay_with_tribute_button": "❤️ Tribute",
|
||||||
"pay_with_stars_button": "🌟 Звезды Telegram",
|
"pay_with_stars_button": "🌟 Звезды Telegram",
|
||||||
"connect_button": "🔗 Подключиться",
|
"connect_button": "🔗 Подключиться",
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ pydantic_settings
|
|||||||
sqlalchemy[asyncio]==2.0.29
|
sqlalchemy[asyncio]==2.0.29
|
||||||
asyncpg==0.29.0
|
asyncpg==0.29.0
|
||||||
alembic==1.13.1
|
alembic==1.13.1
|
||||||
|
aiocryptopay==0.4.8
|
||||||
|
|||||||
Reference in New Issue
Block a user