mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
Add Happ cryptoLink proxy support
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
32
app/external/webhook_server.py
vendored
32
app/external/webhook_server.py
vendored
@@ -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
133
app/utils/happ_links.py
Normal 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>
|
||||
"""
|
||||
@@ -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
|
||||
|
||||
5
main.py
5
main.py
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user