Fix CryptoPay import

This commit is contained in:
Machka Pasla
2025-07-25 12:52:51 +03:00
parent a102903814
commit ce571f6566
10 changed files with 326 additions and 28 deletions
+11 -7
View File
@@ -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
+9 -6
View File
@@ -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.
+44
View File
@@ -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,
+3
View File
@@ -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
View File
@@ -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)
+204
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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",
+1
View File
@@ -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": "🔗 Подключиться",
+1
View File
@@ -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