Add Happ cryptoLink proxy support

This commit is contained in:
Egor
2025-09-25 11:02:27 +03:00
parent 2bd76d118a
commit de8853bd7c
6 changed files with 194 additions and 5 deletions

View File

@@ -291,6 +291,8 @@ CONNECT_BUTTON_HAPP_DOWNLOAD_ENABLED=false
HAPP_DOWNLOAD_LINK_IOS=
HAPP_DOWNLOAD_LINK_ANDROID=
HAPP_DOWNLOAD_LINK_PC=
HAPP_CRYPTOLINK_PROXY_BASE_URL=
HAPP_CRYPTOLINK_PROXY_PATH=/happ-link
# Пропустить принятие правил использования бота
SKIP_RULES_ACCEPT=false

View File

@@ -210,6 +210,8 @@ class Settings(BaseSettings):
CONNECT_BUTTON_MODE: str = "guide"
MINIAPP_CUSTOM_URL: str = ""
CONNECT_BUTTON_HAPP_DOWNLOAD_ENABLED: bool = False
HAPP_CRYPTOLINK_PROXY_BASE_URL: Optional[str] = None
HAPP_CRYPTOLINK_PROXY_PATH: str = "/happ-link"
HAPP_DOWNLOAD_LINK_IOS: Optional[str] = None
HAPP_DOWNLOAD_LINK_ANDROID: Optional[str] = None
HAPP_DOWNLOAD_LINK_PC: Optional[str] = None
@@ -547,9 +549,26 @@ class Settings(BaseSettings):
def get_cryptobot_invoice_expires_seconds(self) -> int:
return self.CRYPTOBOT_INVOICE_EXPIRES_HOURS * 3600
def get_happ_cryptolink_proxy_base_url(self) -> Optional[str]:
base_url = (self.HAPP_CRYPTOLINK_PROXY_BASE_URL or self.WEBHOOK_URL or "").strip()
if not base_url:
return None
return base_url.rstrip('/')
def get_happ_cryptolink_proxy_path(self) -> str:
path = (self.HAPP_CRYPTOLINK_PROXY_PATH or "").strip()
if not path:
path = "/happ-link"
if not path.startswith('/'):
path = f"/{path}"
return path
def is_happ_cryptolink_mode(self) -> bool:
return self.CONNECT_BUTTON_MODE == "happ_cryptolink"
def is_happ_cryptolink_proxy_enabled(self) -> bool:
return self.is_happ_cryptolink_mode() and self.get_happ_cryptolink_proxy_base_url() is not None
def is_happ_download_button_enabled(self) -> bool:
return self.is_happ_cryptolink_mode() and self.CONNECT_BUTTON_HAPP_DOWNLOAD_ENABLED

View File

@@ -8,6 +8,11 @@ from aiohttp import web
from aiogram import Bot
from app.config import settings
from app.utils.happ_links import (
HAPP_LINK_QUERY_PARAM,
decode_happ_link,
render_happ_redirect_page,
)
from app.services.tribute_service import TributeService
from app.services.payment_service import PaymentService
from app.database.database import get_db
@@ -35,9 +40,15 @@ class WebhookServer:
if settings.is_cryptobot_enabled():
self.app.router.add_post(settings.CRYPTOBOT_WEBHOOK_PATH, self._cryptobot_webhook_handler)
self.app.router.add_get('/health', self._health_check)
if settings.is_happ_cryptolink_proxy_enabled():
proxy_path = settings.get_happ_cryptolink_proxy_path()
self.app.router.add_get(proxy_path, self._happ_cryptolink_handler)
self.app.router.add_head(proxy_path, self._happ_cryptolink_handler)
logger.info(f" - Happ cryptoLink proxy: GET {proxy_path}")
self.app.router.add_options(settings.TRIBUTE_WEBHOOK_PATH, self._options_handler)
if settings.is_mulenpay_enabled():
self.app.router.add_options(settings.MULENPAY_WEBHOOK_PATH, self._options_handler)
@@ -50,8 +61,10 @@ class WebhookServer:
logger.info(f" - Mulen Pay webhook: POST {settings.MULENPAY_WEBHOOK_PATH}")
if settings.is_cryptobot_enabled():
logger.info(f" - CryptoBot webhook: POST {settings.CRYPTOBOT_WEBHOOK_PATH}")
if settings.is_happ_cryptolink_proxy_enabled():
logger.info(f" - Happ cryptoLink proxy: GET {settings.get_happ_cryptolink_proxy_path()}")
logger.info(f" - Health check: GET /health")
return self.app
async def start(self):
@@ -177,6 +190,19 @@ class WebhookServer:
logger.error("Отсутствует подпись Mulen Pay webhook")
return False
async def _happ_cryptolink_handler(self, request: web.Request) -> web.Response:
if request.method == 'HEAD':
return web.Response(status=200)
token = request.query.get(HAPP_LINK_QUERY_PARAM, "")
happ_link = decode_happ_link(token)
if not happ_link:
logger.warning("Получен некорректный запрос к Happ proxy: %s", request.query_string)
return web.Response(status=400, text="Invalid or missing link")
html_page = render_happ_redirect_page(happ_link)
return web.Response(text=html_page, content_type='text/html; charset=utf-8')
async def _tribute_webhook_handler(self, request: web.Request) -> web.Response:
try:

133
app/utils/happ_links.py Normal file
View File

@@ -0,0 +1,133 @@
import base64
import html
import logging
from typing import Optional
from app.config import settings
logger = logging.getLogger(__name__)
HAPP_LINK_QUERY_PARAM = "data"
_HAPP_SCHEME_PREFIX = "happ://"
def _encode_happ_link(link: str) -> str:
encoded = base64.urlsafe_b64encode(link.encode("utf-8")).decode("ascii")
return encoded.rstrip("=")
def decode_happ_link(token: str) -> Optional[str]:
if not token:
return None
padding = "=" * (-len(token) % 4)
try:
decoded = base64.urlsafe_b64decode(f"{token}{padding}".encode("ascii")).decode("utf-8")
except (ValueError, UnicodeDecodeError):
logger.warning("Не удалось декодировать cryptoLink из токена")
return None
if not decoded.startswith(_HAPP_SCHEME_PREFIX):
logger.warning("Попытка открыть ссылку с неподдерживаемым протоколом: %s", decoded)
return None
return decoded
def build_happ_proxy_link(crypto_link: str) -> Optional[str]:
base_url = settings.get_happ_cryptolink_proxy_base_url()
if not base_url:
logger.error("Не задан базовый URL для прокси Happ cryptoLink")
return None
path = settings.get_happ_cryptolink_proxy_path()
token = _encode_happ_link(crypto_link)
return f"{base_url}{path}?{HAPP_LINK_QUERY_PARAM}={token}"
def render_happ_redirect_page(happ_link: str) -> str:
escaped_link = html.escape(happ_link, quote=True)
return f"""<!DOCTYPE html>
<html lang=\"ru\">
<head>
<meta charset=\"utf-8\" />
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
<title>Открытие Happ</title>
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: #0d1117;
color: #f0f6fc;
margin: 0;
padding: 24px;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}}
.container {{
max-width: 480px;
width: 100%;
background: rgba(13, 17, 23, 0.85);
border: 1px solid rgba(240, 246, 252, 0.12);
border-radius: 16px;
padding: 28px 24px;
box-shadow: 0 18px 24px rgba(1, 4, 9, 0.45);
text-align: center;
}}
h1 {{
font-size: 1.4rem;
margin-bottom: 12px;
color: #6ca4f7;
}}
p {{
line-height: 1.5;
margin-bottom: 20px;
color: rgba(240, 246, 252, 0.82);
}}
a.button {{
display: inline-block;
padding: 12px 20px;
border-radius: 10px;
background: linear-gradient(135deg, #2788f6, #4c9cf6);
color: #fff;
text-decoration: none;
font-weight: 600;
transition: transform 0.15s ease;
}}
a.button:hover {{
transform: translateY(-2px);
}}
.secondary {{
margin-top: 16px;
font-size: 0.85rem;
color: rgba(240, 246, 252, 0.6);
word-break: break-all;
}}
code {{
display: inline-block;
background: rgba(240, 246, 252, 0.12);
padding: 6px 10px;
border-radius: 6px;
font-size: 0.85rem;
}}
</style>
<script>
function openHapp() {{
window.location.replace('{escaped_link}');
}}
window.addEventListener('load', function () {{
setTimeout(openHapp, 80);
}});
</script>
</head>
<body>
<div class=\"container\">
<h1>Подключение Happ</h1>
<p>Если приложение Happ не открылось автоматически, нажмите кнопку ниже или скопируйте ссылку вручную.</p>
<a class=\"button\" href=\"{escaped_link}\">Открыть в Happ</a>
<p class=\"secondary\">Ссылка для копирования:<br /><code>{escaped_link}</code></p>
</div>
</body>
</html>
"""

View File

@@ -5,6 +5,7 @@ from sqlalchemy import select, delete, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.models import Subscription, User
from app.config import settings
from app.utils.happ_links import build_happ_proxy_link
logger = logging.getLogger(__name__)
@@ -106,6 +107,11 @@ def get_display_subscription_link(subscription: Optional[Subscription]) -> Optio
if settings.is_happ_cryptolink_mode():
crypto_link = getattr(subscription, "subscription_crypto_link", None)
return crypto_link or base_link
if crypto_link:
proxy_link = build_happ_proxy_link(crypto_link)
if proxy_link:
return proxy_link
logger.warning("Не удалось сформировать прокси-ссылку для Happ, возвращаем стандартную ссылку")
return base_link
return base_link

View File

@@ -126,6 +126,7 @@ async def main():
settings.TRIBUTE_ENABLED
or settings.is_cryptobot_enabled()
or settings.is_mulenpay_enabled()
or settings.is_happ_cryptolink_proxy_enabled()
)
if webhook_needed:
@@ -136,7 +137,9 @@ async def main():
enabled_services.append("Mulen Pay")
if settings.is_cryptobot_enabled():
enabled_services.append("CryptoBot")
if settings.is_happ_cryptolink_proxy_enabled():
enabled_services.append("Happ cryptoLink proxy")
logger.info(f"🌐 Запуск webhook сервера для: {', '.join(enabled_services)}...")
webhook_server = WebhookServer(bot)
await webhook_server.start()