feat: install instruction inside web app
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
# ruff: noqa: F401,F403,F405,I001
|
||||
from ._runtime import * # noqa: F403,F405
|
||||
from .guides import warm_subscription_guides_config
|
||||
|
||||
|
||||
def create_subscription_webapp_application(
|
||||
@@ -24,6 +25,8 @@ def create_subscription_webapp_application(
|
||||
app["webapp_logo_cache"] = None
|
||||
app["webapp_logo_cache_lock"] = asyncio.Lock()
|
||||
app["webapp_settings_cache"] = {"ts": 0.0, "data": {}}
|
||||
app["subscription_guides_config_cache"] = {"fingerprint": None, "status": None}
|
||||
app["subscription_guides_config_lock"] = asyncio.Lock()
|
||||
app["webapp_rate_limit_buckets"] = {}
|
||||
app["webapp_rate_limit_lock"] = asyncio.Lock()
|
||||
|
||||
@@ -31,6 +34,7 @@ def create_subscription_webapp_application(
|
||||
await _ensure_shared_http_session()
|
||||
await _warm_webapp_logo_cache(app_obj)
|
||||
await _warm_webapp_animated_emoji_cache(app_obj)
|
||||
await warm_subscription_guides_config(app_obj)
|
||||
|
||||
async def _shutdown(app_obj: web.Application) -> None:
|
||||
await _close_shared_http_session()
|
||||
|
||||
@@ -2,13 +2,31 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any, Awaitable, Callable, Optional
|
||||
|
||||
from bot.infra.redis import cache_delete, redis_key
|
||||
from bot.infra.redis import cache_delete, cache_delete_pattern, redis_key
|
||||
from bot.utils.ttl_cache import AsyncTTLCache
|
||||
from config.settings import Settings
|
||||
|
||||
_WEBAPP_USER_PAYLOAD_CACHES: dict[tuple[int, str, int], AsyncTTLCache] = {}
|
||||
|
||||
|
||||
def reset_webapp_settings_cache(app: Any) -> None:
|
||||
cache = app.get("webapp_settings_cache") if hasattr(app, "get") else None
|
||||
if isinstance(cache, dict):
|
||||
cache["ts"] = 0.0
|
||||
cache["data"] = {}
|
||||
|
||||
|
||||
def reset_subscription_guides_cache(app: Any) -> None:
|
||||
cache = app.get("subscription_guides_config_cache") if hasattr(app, "get") else None
|
||||
if isinstance(cache, dict):
|
||||
cache["fingerprint"] = None
|
||||
cache["status"] = None
|
||||
|
||||
|
||||
def _payload_namespaces(include_devices: bool = False) -> tuple[str, ...]:
|
||||
return ("me", "devices") if include_devices else ("me",)
|
||||
|
||||
|
||||
def _webapp_user_payload_cache(
|
||||
settings: Settings,
|
||||
namespace: str,
|
||||
@@ -55,6 +73,19 @@ def invalidate_local_webapp_user_payload(
|
||||
cache.invalidate(key)
|
||||
|
||||
|
||||
def invalidate_all_local_webapp_user_payloads(
|
||||
settings: Settings,
|
||||
*,
|
||||
include_devices: bool = False,
|
||||
) -> None:
|
||||
namespaces = set(_payload_namespaces(include_devices))
|
||||
for (settings_id, cache_namespace, _ttl), cache in tuple(
|
||||
_WEBAPP_USER_PAYLOAD_CACHES.items()
|
||||
):
|
||||
if settings_id == id(settings) and cache_namespace in namespaces:
|
||||
cache.invalidate()
|
||||
|
||||
|
||||
async def invalidate_webapp_user_caches(
|
||||
settings: Settings,
|
||||
*user_ids: Optional[int],
|
||||
@@ -79,3 +110,17 @@ async def invalidate_webapp_user_caches(
|
||||
invalidate_local_webapp_user_payload(settings, "devices", user_id)
|
||||
if keys:
|
||||
await cache_delete(settings, *keys)
|
||||
|
||||
|
||||
async def invalidate_all_webapp_user_payloads(
|
||||
settings: Settings,
|
||||
*,
|
||||
include_devices: bool = False,
|
||||
) -> None:
|
||||
invalidate_all_local_webapp_user_payloads(settings, include_devices=include_devices)
|
||||
for namespace in _payload_namespaces(include_devices):
|
||||
try:
|
||||
pattern = redis_key(settings, "cache", "webapp", namespace, "*")
|
||||
await cache_delete_pattern(settings, pattern)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from ._runtime import * # noqa: F403,F405
|
||||
|
||||
from bot.app.web.webapp.cache_helpers import (
|
||||
invalidate_local_webapp_user_payload,
|
||||
invalidate_webapp_user_caches as _invalidate_user_payload_caches,
|
||||
)
|
||||
|
||||
|
||||
@@ -26,25 +26,7 @@ async def _invalidate_webapp_user_caches(
|
||||
*user_ids: Optional[int],
|
||||
include_devices: bool = False,
|
||||
) -> None:
|
||||
keys: List[str] = []
|
||||
seen: set[int] = set()
|
||||
for raw_user_id in user_ids:
|
||||
if raw_user_id is None:
|
||||
continue
|
||||
try:
|
||||
user_id = int(raw_user_id)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if user_id in seen:
|
||||
continue
|
||||
seen.add(user_id)
|
||||
keys.append(redis_key(settings, "cache", "webapp", "me", user_id))
|
||||
invalidate_local_webapp_user_payload(settings, "me", user_id)
|
||||
if include_devices:
|
||||
keys.append(redis_key(settings, "cache", "webapp", "devices", user_id))
|
||||
invalidate_local_webapp_user_payload(settings, "devices", user_id)
|
||||
if keys:
|
||||
await cache_delete(settings, *keys)
|
||||
await _invalidate_user_payload_caches(settings, *user_ids, include_devices=include_devices)
|
||||
|
||||
|
||||
def _validation_error_response(exc: ValidationError) -> web.Response:
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
# ruff: noqa: F401,F403,F405,I001
|
||||
from ._runtime import * # noqa: F403,F405
|
||||
|
||||
from config.subscription_guides_config import (
|
||||
SubscriptionGuidesConfigError,
|
||||
subscription_guides_status,
|
||||
validate_panel_subscription_guides_config,
|
||||
)
|
||||
|
||||
PANEL_DEFAULT_SUBPAGE_CONFIG_UUID = "00000000-0000-0000-0000-000000000000"
|
||||
SUBSCRIPTION_GUIDES_CACHE_ERROR_TTL_SECONDS = 30
|
||||
|
||||
|
||||
async def warm_subscription_guides_config(app: web.Application) -> None:
|
||||
try:
|
||||
await _subscription_guides_status_shared(app)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to warm subscription guides config: %s", exc)
|
||||
|
||||
|
||||
async def subscription_guides_route(request: web.Request) -> web.Response:
|
||||
_require_user_id(request)
|
||||
status = await _subscription_guides_status_shared(request.app)
|
||||
payload = {
|
||||
"enabled": bool(status.get("enabled")),
|
||||
"config": status.get("config") if status.get("enabled") else None,
|
||||
"source": status.get("source"),
|
||||
}
|
||||
if status.get("error"):
|
||||
payload["error"] = status["error"]
|
||||
return web.json_response({"ok": True, **payload})
|
||||
|
||||
|
||||
async def public_subscription_guides_route(request: web.Request) -> web.Response:
|
||||
short_uuid = _normalize_short_uuid(request.match_info.get("short_uuid"))
|
||||
if not short_uuid:
|
||||
return web.json_response({"ok": False, "error": "invalid_short_uuid"}, status=404)
|
||||
|
||||
status = await _subscription_guides_status_shared(request.app)
|
||||
subscription = await _public_subscription_payload(request, short_uuid)
|
||||
payload = {
|
||||
"enabled": bool(status.get("enabled")),
|
||||
"config": status.get("config") if status.get("enabled") else None,
|
||||
"source": status.get("source"),
|
||||
"subscription": subscription,
|
||||
}
|
||||
if status.get("error"):
|
||||
payload["error"] = status["error"]
|
||||
return web.json_response({"ok": True, **payload})
|
||||
|
||||
|
||||
async def _subscription_guides_status_shared(app: web.Application) -> Dict[str, Any]:
|
||||
settings: Settings = app["settings"]
|
||||
cache = app.setdefault("subscription_guides_config_cache", {})
|
||||
lock: asyncio.Lock = app.setdefault("subscription_guides_config_lock", asyncio.Lock())
|
||||
fingerprint = _subscription_guides_settings_fingerprint(settings)
|
||||
now = time.monotonic()
|
||||
|
||||
cached = cache.get("status")
|
||||
if cached is not None and cache.get("fingerprint") == fingerprint:
|
||||
if cached.get("enabled") or now - float(cache.get("ts", 0.0)) < (
|
||||
SUBSCRIPTION_GUIDES_CACHE_ERROR_TTL_SECONDS
|
||||
):
|
||||
return cached
|
||||
|
||||
async with lock:
|
||||
cached = cache.get("status")
|
||||
if cached is not None and cache.get("fingerprint") == fingerprint:
|
||||
if cached.get("enabled") or now - float(cache.get("ts", 0.0)) < (
|
||||
SUBSCRIPTION_GUIDES_CACHE_ERROR_TTL_SECONDS
|
||||
):
|
||||
return cached
|
||||
|
||||
status = await _load_subscription_guides_status(app, settings)
|
||||
cache["fingerprint"] = fingerprint
|
||||
cache["status"] = status
|
||||
cache["ts"] = time.monotonic()
|
||||
return status
|
||||
|
||||
|
||||
async def _load_subscription_guides_status(
|
||||
app: web.Application,
|
||||
settings: Settings,
|
||||
) -> Dict[str, Any]:
|
||||
if not bool(getattr(settings, "SUBSCRIPTION_GUIDES_ENABLED", False)):
|
||||
return {"enabled": False, "config": None, "source": None, "error": None}
|
||||
|
||||
admin_json = str(getattr(settings, "SUBSCRIPTION_PAGE_CONFIG_JSON", "") or "").strip()
|
||||
json_override_enabled = bool(
|
||||
getattr(settings, "SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED", False)
|
||||
)
|
||||
if admin_json and json_override_enabled:
|
||||
return subscription_guides_status(settings)
|
||||
|
||||
if bool(getattr(settings, "SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED", True)):
|
||||
panel_status = await _subscription_guides_status_from_panel_config(app, settings)
|
||||
if panel_status.get("enabled"):
|
||||
return panel_status
|
||||
|
||||
return subscription_guides_status(settings)
|
||||
|
||||
|
||||
async def _subscription_guides_status_from_panel_config(
|
||||
app: web.Application,
|
||||
settings: Settings,
|
||||
) -> Dict[str, Any]:
|
||||
panel_service = _panel_service_from_app(app)
|
||||
if panel_service is None:
|
||||
return {
|
||||
"enabled": False,
|
||||
"config": None,
|
||||
"source": "panel",
|
||||
"error": "Panel service is unavailable",
|
||||
}
|
||||
|
||||
try:
|
||||
config_uuid = str(getattr(settings, "SUBSCRIPTION_PAGE_CONFIG_UUID", "") or "").strip()
|
||||
if not config_uuid:
|
||||
config_uuid = await _default_panel_subscription_page_config_uuid(panel_service)
|
||||
config_uuid = config_uuid or PANEL_DEFAULT_SUBPAGE_CONFIG_UUID
|
||||
detail = await panel_service.get_subscription_page_config_by_uuid(config_uuid)
|
||||
if detail is None and config_uuid != PANEL_DEFAULT_SUBPAGE_CONFIG_UUID:
|
||||
detail = await panel_service.get_subscription_page_config_by_uuid(
|
||||
PANEL_DEFAULT_SUBPAGE_CONFIG_UUID
|
||||
)
|
||||
if detail is None:
|
||||
raise SubscriptionGuidesConfigError(
|
||||
f"Panel subscription page config {config_uuid} is unavailable"
|
||||
)
|
||||
config = validate_panel_subscription_guides_config(detail)
|
||||
except (SubscriptionGuidesConfigError, Exception) as exc:
|
||||
logger.warning("Failed to load subscription guides config from Remnawave Panel: %s", exc)
|
||||
return {"enabled": False, "config": None, "source": "panel", "error": str(exc)}
|
||||
|
||||
return {"enabled": True, "config": config, "source": "panel", "error": None}
|
||||
|
||||
|
||||
async def _default_panel_subscription_page_config_uuid(panel_service: Any) -> str:
|
||||
get_list = getattr(panel_service, "get_subscription_page_config_list", None)
|
||||
if not callable(get_list):
|
||||
return ""
|
||||
payload = await get_list()
|
||||
configs = (payload or {}).get("configs")
|
||||
if not isinstance(configs, list):
|
||||
return ""
|
||||
|
||||
candidates: list[Dict[str, Any]] = [item for item in configs if isinstance(item, dict)]
|
||||
for item in candidates:
|
||||
uuid = str(item.get("uuid") or "").strip()
|
||||
if uuid == PANEL_DEFAULT_SUBPAGE_CONFIG_UUID:
|
||||
return uuid
|
||||
|
||||
candidates.sort(key=lambda item: int(item.get("viewPosition") or 0))
|
||||
for item in candidates:
|
||||
uuid = str(item.get("uuid") or "").strip()
|
||||
if uuid:
|
||||
return uuid
|
||||
return ""
|
||||
|
||||
|
||||
async def _public_subscription_payload(
|
||||
request: web.Request,
|
||||
short_uuid: str,
|
||||
) -> Dict[str, Any]:
|
||||
settings: Settings = request.app["settings"]
|
||||
panel_service = _panel_service_from_app(request.app)
|
||||
raw_link = ""
|
||||
username = ""
|
||||
resolved_short_uuid = short_uuid
|
||||
|
||||
async_session_factory: sessionmaker = request.app["async_session_factory"]
|
||||
async with async_session_factory() as session:
|
||||
local_sub = await subscription_dal.get_subscription_by_panel_subscription_uuid(
|
||||
session,
|
||||
short_uuid,
|
||||
)
|
||||
|
||||
if (
|
||||
local_sub
|
||||
and getattr(local_sub, "panel_user_uuid", None)
|
||||
and _local_subscription_is_publicly_active(local_sub)
|
||||
and panel_service
|
||||
):
|
||||
panel_user = await panel_service.get_user_by_uuid(local_sub.panel_user_uuid)
|
||||
if panel_user:
|
||||
raw_link = str(panel_user.get("subscriptionUrl") or "").strip()
|
||||
username = str(panel_user.get("username") or "").strip()
|
||||
resolved_short_uuid = str(panel_user.get("shortUuid") or short_uuid).strip()
|
||||
|
||||
display_link, connect_url = await prepare_config_links(settings, raw_link)
|
||||
return {
|
||||
"active": bool(display_link),
|
||||
"config_link": display_link,
|
||||
"connect_url": connect_url or display_link,
|
||||
"panel_short_uuid": resolved_short_uuid,
|
||||
"username": username,
|
||||
"share_url": _public_install_url(request, resolved_short_uuid),
|
||||
}
|
||||
|
||||
|
||||
def _panel_service_from_app(app: web.Application) -> Any:
|
||||
subscription_service: Optional[SubscriptionService] = app.get("subscription_service")
|
||||
panel_service = (
|
||||
getattr(subscription_service, "panel_service", None) if subscription_service else None
|
||||
)
|
||||
return panel_service or app.get("panel_service")
|
||||
|
||||
|
||||
def _subscription_guides_settings_fingerprint(settings: Settings) -> Tuple[Any, ...]:
|
||||
admin_json = str(getattr(settings, "SUBSCRIPTION_PAGE_CONFIG_JSON", "") or "")
|
||||
return (
|
||||
bool(getattr(settings, "SUBSCRIPTION_GUIDES_ENABLED", False)),
|
||||
bool(getattr(settings, "SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED", True)),
|
||||
bool(getattr(settings, "SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED", False)),
|
||||
str(getattr(settings, "SUBSCRIPTION_PAGE_CONFIG_PATH", "") or ""),
|
||||
str(getattr(settings, "SUBSCRIPTION_PAGE_CONFIG_UUID", "") or ""),
|
||||
hashlib.sha256(admin_json.encode("utf-8")).hexdigest(),
|
||||
str(getattr(settings, "PANEL_API_URL", "") or ""),
|
||||
bool(getattr(settings, "PANEL_API_KEY", "") or ""),
|
||||
)
|
||||
|
||||
|
||||
def _local_subscription_is_publicly_active(subscription: Any) -> bool:
|
||||
end_date = getattr(subscription, "end_date", None)
|
||||
if end_date and end_date.tzinfo is None:
|
||||
end_date = end_date.replace(tzinfo=timezone.utc)
|
||||
return bool(
|
||||
getattr(subscription, "is_active", False)
|
||||
and end_date
|
||||
and end_date > datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
|
||||
def _normalize_short_uuid(value: Any) -> str:
|
||||
short_uuid = str(value or "").strip()
|
||||
if not re.fullmatch(r"[A-Za-z0-9_-]{8,128}", short_uuid):
|
||||
return ""
|
||||
return short_uuid
|
||||
|
||||
|
||||
def _public_install_url(request: web.Request, short_uuid: str) -> str:
|
||||
settings: Settings = request.app["settings"]
|
||||
configured_base = str(getattr(settings, "SUBSCRIPTION_MINI_APP_URL", "") or "").strip()
|
||||
if configured_base:
|
||||
parts = urlsplit(configured_base)
|
||||
if parts.scheme and parts.netloc:
|
||||
base = urlunsplit((parts.scheme, parts.netloc, "", "", ""))
|
||||
else:
|
||||
base = configured_base.rstrip("/")
|
||||
else:
|
||||
host = (
|
||||
request.headers.get("X-Forwarded-Host")
|
||||
or request.headers.get("Host")
|
||||
or request.host
|
||||
)
|
||||
proto = request.headers.get("X-Forwarded-Proto") or request.scheme or "https"
|
||||
base = f"{proto}://{host}"
|
||||
return f"{base.rstrip('/')}/install/share/{quote(short_uuid)}"
|
||||
|
||||
|
||||
def _subscription_page_request_headers(request: web.Request) -> Dict[str, str]:
|
||||
headers = request.headers
|
||||
host = headers.get("X-Forwarded-Host") or headers.get("Host") or request.host
|
||||
proto = headers.get("X-Forwarded-Proto") or request.scheme or "https"
|
||||
user_agent = headers.get(
|
||||
"User-Agent",
|
||||
"Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome Safari",
|
||||
)
|
||||
return {
|
||||
"host": host,
|
||||
"x-forwarded-host": host,
|
||||
"x-forwarded-proto": proto,
|
||||
"user-agent": user_agent,
|
||||
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"accept-language": headers.get("Accept-Language", "ru,en;q=0.9"),
|
||||
"sec-fetch-dest": "document",
|
||||
"sec-fetch-mode": "navigate",
|
||||
"sec-fetch-site": "none",
|
||||
"upgrade-insecure-requests": "1",
|
||||
}
|
||||
@@ -6,6 +6,8 @@ def setup_subscription_webapp_routes(app: web.Application) -> None:
|
||||
app.router.add_get("/", index_route)
|
||||
app.router.add_get("/login/password", index_route)
|
||||
app.router.add_get("/home", index_route)
|
||||
app.router.add_get("/install", index_route)
|
||||
app.router.add_get(r"/install/share/{short_uuid:[A-Za-z0-9_-]{8,128}}", index_route)
|
||||
app.router.add_get("/invite", index_route)
|
||||
app.router.add_get("/devices", index_route)
|
||||
app.router.add_get("/settings", index_route)
|
||||
@@ -60,6 +62,11 @@ def setup_subscription_webapp_routes(app: web.Application) -> None:
|
||||
app.router.add_get("/api/bootstrap", bootstrap_route)
|
||||
app.router.add_get("/api/i18n", i18n_route)
|
||||
app.router.add_get("/api/me", me_route)
|
||||
app.router.add_get("/api/subscription-guides", subscription_guides_route)
|
||||
app.router.add_get(
|
||||
r"/api/subscription-guides/public/{short_uuid:[A-Za-z0-9_-]{8,128}}",
|
||||
public_subscription_guides_route,
|
||||
)
|
||||
app.router.add_get("/api/account/avatar", account_avatar_route)
|
||||
app.router.add_post("/api/account/language", account_language_route)
|
||||
app.router.add_post("/api/account/email/request", account_email_request_route)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# ruff: noqa: F401,F403,F405,I001
|
||||
from ._runtime import * # noqa: F403,F405
|
||||
|
||||
from config.subscription_guides_config import subscription_guides_available
|
||||
from config.webapp_themes_config import public_themes_catalog_payload
|
||||
|
||||
|
||||
@@ -82,7 +83,7 @@ async def _build_user_payload(request: web.Request, user_id: int) -> Dict[str, A
|
||||
"language_code": lang,
|
||||
"is_admin": is_admin,
|
||||
},
|
||||
"subscription": _serialize_subscription(settings, active, local_sub, lang),
|
||||
"subscription": _serialize_subscription(request, settings, active, local_sub, lang),
|
||||
"referral": {
|
||||
"code": referral_code,
|
||||
"bot_link": referral_link,
|
||||
@@ -132,6 +133,7 @@ async def _build_user_payload(request: web.Request, user_id: int) -> Dict[str, A
|
||||
"trial_traffic_limit_gb": float(settings.TRIAL_TRAFFIC_LIMIT_GB or 0),
|
||||
"trial_traffic_strategy": getattr(settings, "TRIAL_TRAFFIC_STRATEGY", "NO_RESET"),
|
||||
"subscription_purchase_description": settings.subscription_purchase_description(lang),
|
||||
"subscription_guides_enabled": subscription_guides_available(settings),
|
||||
"email_auth_enabled": settings.email_auth_configured,
|
||||
},
|
||||
}
|
||||
@@ -179,6 +181,7 @@ def _build_webapp_referral_link(
|
||||
|
||||
|
||||
def _serialize_subscription(
|
||||
request: web.Request,
|
||||
settings: Settings,
|
||||
active: Optional[Dict[str, Any]],
|
||||
local_sub: Optional[Any],
|
||||
@@ -192,6 +195,8 @@ def _serialize_subscription(
|
||||
"days_left": 0,
|
||||
"config_link": None,
|
||||
"connect_url": None,
|
||||
"panel_short_uuid": None,
|
||||
"install_share_url": None,
|
||||
}
|
||||
|
||||
end_date = active.get("end_date")
|
||||
@@ -231,6 +236,7 @@ def _serialize_subscription(
|
||||
can_topup_traffic = False
|
||||
can_topup_devices = False
|
||||
|
||||
panel_short_uuid = str(active.get("panel_short_uuid") or "").strip()
|
||||
return {
|
||||
"active": seconds_left > 0,
|
||||
"status": active.get("status_from_panel") or "UNKNOWN",
|
||||
@@ -240,6 +246,8 @@ def _serialize_subscription(
|
||||
"remaining_text": _format_remaining(seconds_left, lang),
|
||||
"config_link": active.get("config_link"),
|
||||
"connect_url": active.get("connect_button_url") or active.get("config_link"),
|
||||
"panel_short_uuid": panel_short_uuid or None,
|
||||
"install_share_url": _build_install_share_link(request, settings, panel_short_uuid),
|
||||
"traffic_limit": _format_bytes(active.get("traffic_limit_bytes"), zero_as_unlimited=True),
|
||||
"traffic_used": _format_bytes(active.get("traffic_used_bytes")),
|
||||
"traffic_limit_bytes": _coerce_int_or_none(active.get("traffic_limit_bytes")),
|
||||
@@ -284,6 +292,32 @@ def _serialize_subscription(
|
||||
}
|
||||
|
||||
|
||||
def _build_install_share_link(
|
||||
request: web.Request,
|
||||
settings: Settings,
|
||||
short_uuid: str,
|
||||
) -> Optional[str]:
|
||||
short_uuid = str(short_uuid or "").strip()
|
||||
if not short_uuid:
|
||||
return None
|
||||
configured_base = str(getattr(settings, "SUBSCRIPTION_MINI_APP_URL", "") or "").strip()
|
||||
if configured_base:
|
||||
parts = urlsplit(configured_base)
|
||||
if parts.scheme and parts.netloc:
|
||||
base = urlunsplit((parts.scheme, parts.netloc, "", "", ""))
|
||||
else:
|
||||
base = configured_base.rstrip("/")
|
||||
else:
|
||||
host = (
|
||||
request.headers.get("X-Forwarded-Host")
|
||||
or request.headers.get("Host")
|
||||
or request.host
|
||||
)
|
||||
proto = request.headers.get("X-Forwarded-Proto") or request.scheme or "https"
|
||||
base = f"{proto}://{host}"
|
||||
return f"{base.rstrip('/')}/install/share/{quote(short_uuid)}"
|
||||
|
||||
|
||||
def _serialize_plans(
|
||||
settings: Settings,
|
||||
lang: str,
|
||||
|
||||
Reference in New Issue
Block a user