From b67f659746ca20207148844f79c31daa965ed70f Mon Sep 17 00:00:00 2001 From: Machka Pasla <161734431+machka-pasla@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:16:39 +0300 Subject: [PATCH] Refine CryptoBot labels and .env layout --- .env.example | 18 ++- README.md | 15 +- bot/handlers/user/subscription.py | 44 ++++++ bot/keyboards/inline/user_keyboards.py | 3 + bot/main_bot.py | 30 +++- bot/services/crypto_pay_service.py | 203 +++++++++++++++++++++++++ config/settings.py | 37 +++-- locales/en.json | 1 + locales/ru.json | 1 + requirements.txt | 1 + 10 files changed, 325 insertions(+), 28 deletions(-) create mode 100644 bot/services/crypto_pay_service.py diff --git a/.env.example b/.env.example index 29816ff..9f22269 100644 --- a/.env.example +++ b/.env.example @@ -19,24 +19,30 @@ TERMS_OF_SERVICE_URL=https://example.com/tos SUBSCRIPTION_MINI_APP_URL= START_COMMAND_DESCRIPTION= +# Webhook Base URL (used for Telegram and payment providers) +WEBHOOK_BASE_URL=https://webhooks.yourdomain.tld + # YooKassa Payment Gateway Configuration YOOKASSA_SHOP_ID=your_shop_id YOOKASSA_SECRET_KEY=your_secret_key -YOOKASSA_WEBHOOK_BASE_URL=https://webhooks.yourdomain.tld YOOKASSA_RETURN_URL=https://t.me/your_bot YOOKASSA_DEFAULT_RECEIPT_EMAIL=your_email@example.com YOOKASSA_VAT_CODE=1 YOOKASSA_PAYMENT_MODE=full_prepayment YOOKASSA_PAYMENT_SUBJECT=payment -# Telegram Webhook Configuration (Optional) -# If unset, bot will use polling while YooKassa uses webhooks. -TELEGRAM_WEBHOOK_BASE_URL=https://webhooks.yourdomain.tld +# CryptoBot Payment Gateway Configuration +CRYPTOPAY_TOKEN= +CRYPTOPAY_NETWORK=mainnet +CRYPTOPAY_ASSET=TON +# API key for verifying Tribute webhook signatures +TRIBUTE_API_KEY= -# Payment Methods +# Payment Method Toggles YOOKASSA_ENABLED=True STARS_ENABLED=True TRIBUTE_ENABLED=True +CRYPTOPAY_ENABLED=True # Subscription Options 1_MONTH_ENABLED=True @@ -58,8 +64,6 @@ TRIBUTE_LINK_6_MONTHS= RUB_PRICE_12_MONTHS=900 STARS_PRICE_12_MONTHS=0 TRIBUTE_LINK_12_MONTHS= -# API key for verifying Tribute webhook signatures -TRIBUTE_API_KEY= # Subscription Notifications SUBSCRIPTION_NOTIFICATIONS_ENABLED=True diff --git a/README.md b/README.md index 9ef64e2..8610a01 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ This Telegram bot is designed to automate the sale and management of subscriptio * **Subscription Management:** * Handles subscription purchases for various periods (1, 3, 6, 12 months). * Integrates with **YooKassa** for payment processing, including fiscal receipt data. + * Supports **Crypto Pay** for cryptocurrency payments. * Automatic subscription activation/extension upon successful payment. * 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. @@ -88,14 +89,14 @@ This Telegram bot is designed to automate the sale and management of subscriptio * **YooKassa Settings:** * `YOOKASSA_SHOP_ID`: Your shop ID 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_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_PAYMENT_MODE`: e.g., `full_prepayment`. * `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. - * **Payment Method Toggles:** `YOOKASSA_ENABLED`, `STARS_ENABLED`, `TRIBUTE_ENABLED`. + * **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`, `CRYPTOPAY_ENABLED`. * **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` (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):** * **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`). - * If using Telegram webhooks, forward requests for `https://{TELEGRAM_WEBHOOK_BASE_URL_domain}/` to `http://remnawave-tg-shop:{WEB_SERVER_PORT}/`. - * **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/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/cryptopay` to `http://remnawave-tg-shop:{WEB_SERVER_PORT}/webhook/cryptopay`. + * 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}/` to `http://remnawave-tg-shop:{WEB_SERVER_PORT}/`. + * **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:** * 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. diff --git a/bot/handlers/user/subscription.py b/bot/handlers/user/subscription.py index e10cbd7..d4be011 100644 --- a/bot/handlers/user/subscription.py +++ b/bot/handlers/user/subscription.py @@ -13,6 +13,7 @@ from bot.keyboards.inline.user_keyboards import ( get_payment_url_keyboard, get_back_to_main_menu_markup) from bot.services.yookassa_service import YooKassaService 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.panel_api_service import PanelApiService from bot.services.referral_service import ReferralService @@ -293,6 +294,49 @@ async def pay_yk_callback_handler( 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") async def reshow_subscription_options_callback(callback: types.CallbackQuery, i18n_data: dict, diff --git a/bot/keyboards/inline/user_keyboards.py b/bot/keyboards/inline/user_keyboards.py index 2fb1953..49337cd 100644 --- a/bot/keyboards/inline/user_keyboards.py +++ b/bot/keyboards/inline/user_keyboards.py @@ -134,6 +134,9 @@ def get_payment_method_keyboard(months: int, price: float, if settings.YOOKASSA_ENABLED: builder.button(text=_("pay_with_yookassa_button"), 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"), callback_data="main_action:subscribe") builder.adjust(1) diff --git a/bot/main_bot.py b/bot/main_bot.py index 1e33528..b843387 100644 --- a/bot/main_bot.py +++ b/bot/main_bot.py @@ -38,6 +38,7 @@ from bot.services.referral_service import ReferralService from bot.services.promo_code_service import PromoCodeService from bot.services.stars_service import StarsService 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 @@ -101,7 +102,7 @@ async def on_startup_configured(dispatcher: Dispatcher): 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: full_telegram_webhook_url = ( f"{str(telegram_webhook_url_to_set).rstrip('/')}/{settings.BOT_TOKEN}" @@ -152,7 +153,7 @@ async def on_startup_configured(dispatcher: Dispatcher): ) else: 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) @@ -265,6 +266,16 @@ async def run_bot(settings_param: Settings): stars_service = StarsService( 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( bot, settings_param, @@ -288,6 +299,7 @@ async def run_bot(settings_param: Settings): dp["referral_service"] = referral_service dp["promo_code_service"] = promo_code_service dp["stars_service"] = stars_service + dp["cryptopay_service"] = cryptopay_service dp["tribute_service"] = tribute_service dp["panel_webhook_service"] = panel_webhook_service 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) - tg_webhook_base = getattr(settings_param, "TELEGRAM_WEBHOOK_BASE_URL", None) - yk_webhook_base = getattr(settings_param, "YOOKASSA_WEBHOOK_BASE_URL", None) + tg_webhook_base = settings_param.WEBHOOK_BASE_URL + yk_webhook_base = settings_param.WEBHOOK_BASE_URL should_run_aiohttp_server = bool(tg_webhook_base) or ( 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"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( - 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 Telegram Polling: {run_telegram_polling}") @@ -343,6 +355,7 @@ async def run_bot(settings_param: Settings): app["referral_service"] = referral_service app["panel_service"] = panel_service app["stars_service"] = stars_service + app["cryptopay_service"] = cryptopay_service app["tribute_service"] = tribute_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) 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 if panel_path.startswith("/"): app.router.add_post(panel_path, panel_webhook_route) diff --git a/bot/services/crypto_pay_service.py b/bot/services/crypto_pay_service.py new file mode 100644 index 0000000..b28d411 --- /dev/null +++ b/bot/services/crypto_pay_service.py @@ -0,0 +1,203 @@ +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, 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) diff --git a/config/settings.py b/config/settings.py index 5a15223..d8c8e97 100644 --- a/config/settings.py +++ b/config/settings.py @@ -26,7 +26,6 @@ class Settings(BaseSettings): YOOKASSA_SHOP_ID: Optional[str] = None YOOKASSA_SECRET_KEY: Optional[str] = None - YOOKASSA_WEBHOOK_BASE_URL: Optional[str] = None YOOKASSA_RETURN_URL: Optional[str] = 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_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) STARS_ENABLED: bool = Field(default=True) @@ -169,9 +173,9 @@ class Settings(BaseSettings): @computed_field @property def yookassa_full_webhook_url(self) -> Optional[str]: - if self.YOOKASSA_WEBHOOK_BASE_URL: - - return f"{self.YOOKASSA_WEBHOOK_BASE_URL.rstrip('/')}{self.yookassa_webhook_path}" + base = self.WEBHOOK_BASE_URL + if base: + return f"{base.rstrip('/')}{self.yookassa_webhook_path}" return None @computed_field @@ -182,8 +186,9 @@ class Settings(BaseSettings): @computed_field @property def tribute_full_webhook_url(self) -> Optional[str]: - if self.YOOKASSA_WEBHOOK_BASE_URL: - return f"{self.YOOKASSA_WEBHOOK_BASE_URL.rstrip('/')}{self.tribute_webhook_path}" + base = self.WEBHOOK_BASE_URL + if base: + return f"{base.rstrip('/')}{self.tribute_webhook_path}" return None @computed_field @@ -194,8 +199,22 @@ class Settings(BaseSettings): @computed_field @property def panel_full_webhook_url(self) -> Optional[str]: - if self.YOOKASSA_WEBHOOK_BASE_URL: - return f"{self.YOOKASSA_WEBHOOK_BASE_URL.rstrip('/')}{self.panel_webhook_path}" + base = self.WEBHOOK_BASE_URL + 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 @computed_field diff --git a/locales/en.json b/locales/en.json index a4bab78..aac840b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -25,6 +25,7 @@ "choose_payment_method": "Choose payment method:", "pay_button": "💳 Pay", "pay_with_yookassa_button": "💳 YooKassa", + "pay_with_cryptopay_button": "💎 CryptoBot", "pay_with_tribute_button": "❤️ Tribute", "pay_with_stars_button": "🌟 Telegram Stars", "connect_button": "🔗 Connect", diff --git a/locales/ru.json b/locales/ru.json index 787edef..1c9c209 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -25,6 +25,7 @@ "choose_payment_method": "Выберите способ оплаты:", "pay_button": "💳 Оплатить", "pay_with_yookassa_button": "💳 ЮKassa", + "pay_with_cryptopay_button": "💎 CryptoBot", "pay_with_tribute_button": "❤️ Tribute", "pay_with_stars_button": "🌟 Звезды Telegram", "connect_button": "🔗 Подключиться", diff --git a/requirements.txt b/requirements.txt index d9133af..14c663e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ pydantic_settings sqlalchemy[asyncio]==2.0.29 asyncpg==0.29.0 alembic==1.13.1 +aiocryptopay==0.4.8