208 lines
7.8 KiB
Python
208 lines
7.8 KiB
Python
import asyncio
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from config import HAPP_CRYPTOLINK, LEGACY_LINKS, PUBLIC_LINK, SUPERNODE
|
|
from core.bootstrap import MODES_CONFIG
|
|
from database import filter_cluster_by_subgroup, get_key_details, get_tariff_by_id
|
|
from logger import logger
|
|
from panels._3xui import get_vless_link_for_client, get_xui_instance
|
|
from panels.remnawave_runtime import with_remnawave_api
|
|
from .utils import is_plan_vless, split_by_panel
|
|
|
|
|
|
async def _is_vless_tariff(session: AsyncSession, email: str) -> bool:
|
|
kd = await get_key_details(session, email)
|
|
if not kd or not kd.get("tariff_id"):
|
|
return False
|
|
tariff = await get_tariff_by_id(session, int(kd["tariff_id"]))
|
|
if not tariff:
|
|
return False
|
|
return is_plan_vless(tariff)
|
|
|
|
|
|
async def _try_build_remna_vless(
|
|
session: AsyncSession,
|
|
servers: list,
|
|
email: str,
|
|
) -> tuple[str | None, str | None, str | None]:
|
|
happ_cryptolink_enabled = bool(MODES_CONFIG.get("HAPP_CRYPTOLINK_ENABLED", HAPP_CRYPTOLINK))
|
|
|
|
si = servers[0]
|
|
server_ref = si.get("server_name") or si.get("cluster_name") or si.get("api_url") or ""
|
|
|
|
async def _build(api):
|
|
data = await api.get_subscription_by_username(email)
|
|
if not data:
|
|
return None
|
|
|
|
sub_url = data.get("subscriptionUrl") or None
|
|
links = data.get("links") or []
|
|
|
|
best_vless = None
|
|
for link in links:
|
|
if isinstance(link, str) and link.lower().startswith("vless://"):
|
|
best_vless = link
|
|
logger.debug(f"[Remnawave] Found VLESS in links: {link[:60]}...")
|
|
break
|
|
|
|
happ_link = None
|
|
if happ_cryptolink_enabled and sub_url:
|
|
try:
|
|
happ_link = await api.encrypt_happ_crypto_link(sub_url)
|
|
except Exception as e:
|
|
logger.warning(f"[Remnawave] happ encrypt failed: {e}")
|
|
happ_link = None
|
|
|
|
return best_vless, sub_url, happ_link
|
|
|
|
result = await with_remnawave_api(session, str(server_ref), _build, fallback_any=True, timeout_sec=8.0)
|
|
if result is None:
|
|
logger.warning("[Remnawave] login failed")
|
|
return None, None, None
|
|
return result
|
|
|
|
|
|
async def _try_build_3xui_vless(servers: list, email: str) -> str | None:
|
|
async def one(si: dict) -> str | None:
|
|
name = si.get("server_name", "unknown")
|
|
inbound_id = si.get("inbound_id")
|
|
if not inbound_id:
|
|
return None
|
|
login_email = f"{email}_{name.lower()}" if SUPERNODE else email
|
|
try:
|
|
xui = await get_xui_instance(si["api_url"])
|
|
except Exception as e:
|
|
logger.warning(f"[{name}] 3x-ui недоступен для VLESS: {e}")
|
|
return None
|
|
try:
|
|
inbound = await xui.inbound.get_by_id(int(inbound_id))
|
|
if not inbound:
|
|
return None
|
|
port = getattr(inbound, "port", None)
|
|
from servers import extract_host
|
|
host = extract_host(si.get("subscription_url") or si.get("api_url"))
|
|
return await get_vless_link_for_client(
|
|
xui=xui,
|
|
inbound_id=int(inbound_id),
|
|
email=login_email,
|
|
external_host=host,
|
|
port=int(port) if port else None,
|
|
remark=email,
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"[{name}] ошибка VLESS: {e}")
|
|
return None
|
|
|
|
results = await asyncio.gather(*[one(s) for s in servers], return_exceptions=True)
|
|
return next((r for r in results if isinstance(r, str) and r), None)
|
|
|
|
|
|
async def make_aggregated_link(
|
|
session: AsyncSession,
|
|
cluster_all: list,
|
|
cluster_id: str,
|
|
email: str,
|
|
client_id: str,
|
|
tg_id: int,
|
|
subgroup_code: str | None = None,
|
|
remna_link_override: str | None = None,
|
|
plan=None,
|
|
) -> str | None:
|
|
legacy_links_enabled = bool(MODES_CONFIG.get("LEGACY_LINKS_ENABLED", LEGACY_LINKS))
|
|
|
|
servers = (
|
|
await filter_cluster_by_subgroup(session, cluster_all, subgroup_code, cluster_id, tariff_id=plan)
|
|
if subgroup_code
|
|
else cluster_all
|
|
)
|
|
if not servers:
|
|
logger.info("[agg_link] servers=0 after DB filter")
|
|
return None
|
|
|
|
xui, remna = split_by_panel(servers)
|
|
logger.debug(f"[agg_link] subgroup='{subgroup_code}' xui={len(xui)} remna={len(remna)}")
|
|
|
|
if plan is None:
|
|
vless_needed = await _is_vless_tariff(session, email)
|
|
elif isinstance(plan, int):
|
|
tr = await get_tariff_by_id(session, plan)
|
|
vless_needed = is_plan_vless(tr)
|
|
else:
|
|
vless_needed = is_plan_vless(plan)
|
|
|
|
base = PUBLIC_LINK.rstrip("/")
|
|
|
|
if vless_needed:
|
|
if legacy_links_enabled:
|
|
if xui:
|
|
xui_link = await _try_build_3xui_vless(xui, email)
|
|
if xui_link:
|
|
logger.info("[agg_link] LEGACY choose 3x-ui VLESS")
|
|
return xui_link
|
|
logger.info("[agg_link] LEGACY fallback base")
|
|
return f"{base}/{email}/{tg_id}"
|
|
if xui:
|
|
xui_link = await _try_build_3xui_vless(xui, email)
|
|
if xui_link:
|
|
logger.info("[agg_link] choose 3x-ui VLESS")
|
|
return xui_link
|
|
if remna:
|
|
best_vless, sub_url, happ_link = await _try_build_remna_vless(session, remna, email)
|
|
if best_vless:
|
|
logger.info("[agg_link] choose Remnawave VLESS")
|
|
return best_vless
|
|
if remna_link_override and remna_link_override.lower().startswith("vless://"):
|
|
logger.info("[agg_link] choose override Remnawave VLESS")
|
|
return remna_link_override
|
|
kd = await get_key_details(session, email)
|
|
stored = kd.get("remnawave_link") if kd else None
|
|
if stored and str(stored).lower().startswith("vless://"):
|
|
logger.info("[agg_link] choose stored Remnawave VLESS")
|
|
return stored
|
|
if happ_link:
|
|
logger.info("[agg_link] choose Remnawave cryptoLink (vless)")
|
|
return happ_link
|
|
if sub_url:
|
|
logger.info("[agg_link] choose Remnawave subscriptionUrl (vless)")
|
|
return sub_url
|
|
logger.info("[agg_link] fallback base link")
|
|
return f"{base}/{email}/{tg_id}"
|
|
|
|
if remna and not xui:
|
|
if legacy_links_enabled:
|
|
logger.info("[agg_link] LEGACY non-vless -> base link")
|
|
return f"{base}/{email}/{tg_id}"
|
|
|
|
happ_cryptolink_enabled = bool(MODES_CONFIG.get("HAPP_CRYPTOLINK_ENABLED", HAPP_CRYPTOLINK))
|
|
|
|
if remna_link_override and (
|
|
remna_link_override.lower().startswith("vless://") or remna_link_override.startswith(("http", "happ://"))
|
|
):
|
|
if not happ_cryptolink_enabled:
|
|
logger.info("[agg_link] choose override Remnawave (non-vless)")
|
|
return remna_link_override
|
|
|
|
best_vless, sub_url, happ_link = await _try_build_remna_vless(session, remna, email)
|
|
if happ_link:
|
|
logger.info("[agg_link] choose Remnawave cryptoLink (non-vless)")
|
|
return happ_link
|
|
if remna_link_override and (
|
|
remna_link_override.lower().startswith("vless://") or remna_link_override.startswith(("http", "happ://"))
|
|
):
|
|
logger.info("[agg_link] choose override Remnawave (non-vless)")
|
|
return remna_link_override
|
|
kd = await get_key_details(session, email)
|
|
stored = kd.get("remnawave_link") if kd else None
|
|
if stored:
|
|
logger.info("[agg_link] choose stored Remnawave (non-vless)")
|
|
return stored
|
|
if sub_url:
|
|
logger.info("[agg_link] choose Remnawave subscriptionUrl (non-vless)")
|
|
return sub_url
|
|
if best_vless:
|
|
logger.info("[agg_link] fallback Remnawave VLESS (non-vless)")
|
|
return best_vless
|
|
|
|
return f"{base}/{email}/{tg_id}"
|