import asyncio from datetime import datetime from sqlalchemy.ext.asyncio import AsyncSession from config import HAPP_CRYPTOLINK, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, SUPERNODE from core.bootstrap import MODES_CONFIG from database import filter_cluster_by_subgroup, update_key_client_id from logger import ( CLOGGER as logger, PANEL_REMNA, PANEL_XUI, ) from panels._3xui import ClientConfig, add_client, extend_client_key, get_xui_instance from panels.remnawave import RemnawaveAPI from ..keys.operations.deletion import delete_on_3xui, delete_on_remnawave from ..keys.operations.utils import bytes_from_gb, norm_name, split_by_panel async def ensure_on_remnawave( servers: list, email: str, client_id: str, tg_id: int, new_expiry_time: int, total_gb: int, hwid_device_limit: int, reset_traffic: bool, attempt_update_first: bool, external_squad_uuid: str | None = None, ) -> tuple[str | None, str | None]: if not servers: return None, None if total_gb is None: total_gb = 0 if hwid_device_limit is None: hwid_device_limit = 0 else: hwid_device_limit = int(hwid_device_limit) inbounds = [s.get("inbound_id") for s in servers if s.get("inbound_id")] api = RemnawaveAPI(servers[0]["api_url"]) ok = await api.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD) if not ok: logger.error(f"{PANEL_REMNA} API недоступен при создании/обновлении") return None, None expire_iso = datetime.utcfromtimestamp(new_expiry_time // 1000).isoformat() + "Z" traffic_bytes = bytes_from_gb(total_gb) use_crypto_link = bool(MODES_CONFIG.get("HAPP_CRYPTOLINK_ENABLED", HAPP_CRYPTOLINK)) async def _build_link_from_subscription(sub_url: str | None) -> str | None: if not sub_url: return None remna_link = None if use_crypto_link: try: remna_link = await api.encrypt_happ_crypto_link(sub_url) except Exception as e: logger.error(f"{PANEL_REMNA} ошибка шифрования happ-ссылки: {e}") remna_link = None if not remna_link: remna_link = sub_url return remna_link async def do_update(): try: updated = await api.update_user( uuid=client_id, expire_at=expire_iso, active_user_inbounds=inbounds, traffic_limit_bytes=traffic_bytes, hwid_device_limit=hwid_device_limit, external_squad_uuid=external_squad_uuid, ) if not updated: return None, None if reset_traffic: try: await api.reset_user_traffic(client_id) except Exception: pass remna_link = None try: sub_data = await api.get_subscription_by_username(email) except Exception as e: logger.error(f"{PANEL_REMNA} get_subscription_by_username при update: {e}") sub_data = None if sub_data: sub_url = sub_data.get("subscriptionUrl") remna_link = await _build_link_from_subscription(sub_url) return client_id, remna_link except Exception as e: logger.error(f"{PANEL_REMNA} update_user не удалось: {e}") return None, None async def do_create(): try: payload = { "username": email, "trafficLimitStrategy": "NO_RESET", "expireAt": expire_iso, "telegramId": tg_id, "activeInternalSquads": inbounds, "uuid": client_id, } if traffic_bytes > 0: payload["trafficLimitBytes"] = traffic_bytes payload["hwidDeviceLimit"] = hwid_device_limit if external_squad_uuid is not None: payload["externalSquadUuid"] = external_squad_uuid or None created = await api.create_user(payload) if not created: return None, None user = created.get("user") or {} new_uuid = user.get("uuid") or client_id sub_url = created.get("subscriptionUrl") remna_link = await _build_link_from_subscription(sub_url) return new_uuid, remna_link except Exception as e: logger.error(f"{PANEL_REMNA} создание не удалось: {e}") return None, None if attempt_update_first: updated_id, link = await do_update() if updated_id: return updated_id, link return await do_create() created_id, link = await do_create() if created_id: return created_id, link return await do_update() async def ensure_on_3xui( servers: list, email: str, client_id: str, tg_id: int, new_expiry_time: int, total_gb: int, hwid_device_limit: int, attempt_update_first: bool, ): tasks = [] traffic = bytes_from_gb(total_gb) for s in servers: name = s.get("server_name", "unknown") inbound_id = s.get("inbound_id") if not inbound_id: logger.warning(f"{PANEL_XUI} [{name}] INBOUND_ID отсутствует") continue login_email = f"{email}_{name.lower()}" if SUPERNODE else email sub_id = email if SUPERNODE else login_email async def one(si, nm, inbound, login, sub): try: xui = await get_xui_instance(si["api_url"]) except Exception as e: logger.error(f"{PANEL_XUI} [{nm}] API недоступен: {e}") return async def do_update(): try: updated = await extend_client_key( xui=xui, inbound_id=int(inbound), email=login, new_expiry_time=new_expiry_time, client_id=client_id, total_gb=traffic, sub_id=sub, tg_id=tg_id, limit_ip=hwid_device_limit, ) return bool(updated) except Exception as e: logger.error(f"{PANEL_XUI} [{nm}] ошибка продления: {e}") return False async def do_create(): try: cfg = ClientConfig( client_id=client_id, email=login, tg_id=tg_id, limit_ip=hwid_device_limit, total_gb=traffic, expiry_time=new_expiry_time, enable=True, flow="xtls-rprx-vision", inbound_id=int(inbound), sub_id=sub, ) created = await add_client(xui, cfg) if not created: logger.error(f"{PANEL_XUI} [{nm}] add_client вернул False") return bool(created) except Exception as e: logger.error(f"{PANEL_XUI} [{nm}] ошибка add_client: {e}") return False if attempt_update_first: if await do_update(): return await do_create() else: if await do_create(): return await do_update() tasks.append(one(s, name, inbound_id, login_email, sub_id)) if tasks: await asyncio.gather(*tasks, return_exceptions=True) async def migrate_between_subgroups( session: AsyncSession, cluster_all: list, cluster_id: str, email: str, client_id: str, tg_id: int, new_expiry_time: int, total_gb: int, hwid_device_limit: int, reset_traffic: bool, old_subgroup: str, target_subgroup: str, external_squad_uuid: str | None = None, tariff_id: int | None = None, ) -> tuple[str, str | None]: target = await filter_cluster_by_subgroup(session, cluster_all, target_subgroup, cluster_id, tariff_id=tariff_id) xui_tgt, remna_tgt = split_by_panel(target) old_set = await filter_cluster_by_subgroup(session, cluster_all, old_subgroup, cluster_id) was_on_remna_before = any( s.get("enabled", True) and (s.get("panel_type", "").lower() == "remnawave") for s in old_set ) was_on_xui_before = any(s.get("enabled", True) and (s.get("panel_type", "").lower() == "3x-ui") for s in old_set) xui_target_names = {norm_name(s.get("server_name")) for s in xui_tgt} remna_target_urls = {(s.get("api_url") or "").rstrip("/") for s in remna_tgt} xui_old = [s for s in old_set if s.get("enabled", True) and (s.get("panel_type", "").lower() == "3x-ui")] remna_old = [s for s in old_set if s.get("enabled", True) and (s.get("panel_type", "").lower() == "remnawave")] xui_old_non = [s for s in xui_old if norm_name(s.get("server_name")) not in xui_target_names] remna_old_non = [s for s in remna_old if (s.get("api_url") or "").rstrip("/") not in remna_target_urls] if not target: if xui_old: await delete_on_3xui(xui_old, email, client_id) if remna_old: await delete_on_remnawave(remna_old, client_id) return client_id, None if xui_tgt and not remna_tgt: await ensure_on_3xui( servers=xui_tgt, email=email, client_id=client_id, tg_id=tg_id, new_expiry_time=new_expiry_time, total_gb=total_gb, hwid_device_limit=hwid_device_limit, attempt_update_first=was_on_xui_before, ) if xui_old_non: await delete_on_3xui(xui_old_non, email, client_id) if remna_old_non: await delete_on_remnawave(remna_old_non, client_id) return client_id, None if remna_tgt and not xui_tgt: if xui_old_non: await delete_on_3xui(xui_old_non, email, client_id) new_remna_id, remna_link = await ensure_on_remnawave( servers=remna_tgt, email=email, client_id=client_id, tg_id=tg_id, new_expiry_time=new_expiry_time, total_gb=total_gb, hwid_device_limit=hwid_device_limit, reset_traffic=reset_traffic, attempt_update_first=was_on_remna_before, external_squad_uuid=external_squad_uuid, ) if remna_old_non: await delete_on_remnawave(remna_old_non, client_id) if new_remna_id and new_remna_id != client_id: await update_key_client_id(session, email, new_remna_id) client_id = new_remna_id return client_id, remna_link if xui_old_non: await delete_on_3xui(xui_old_non, email, client_id) old_id = client_id new_remna_id, remna_link = await ensure_on_remnawave( servers=remna_tgt, email=email, client_id=client_id, tg_id=tg_id, new_expiry_time=new_expiry_time, total_gb=total_gb, hwid_device_limit=hwid_device_limit, reset_traffic=reset_traffic, attempt_update_first=was_on_remna_before, external_squad_uuid=external_squad_uuid, ) if remna_old_non: await delete_on_remnawave(remna_old_non, old_id) if new_remna_id and new_remna_id != old_id: await update_key_client_id(session, email, new_remna_id) client_id = new_remna_id await delete_on_3xui(xui_tgt, email, old_id) await ensure_on_3xui( servers=xui_tgt, email=email, client_id=client_id, tg_id=tg_id, new_expiry_time=new_expiry_time, total_gb=total_gb, hwid_device_limit=hwid_device_limit, attempt_update_first=False, ) return client_id, remna_link await ensure_on_3xui( servers=xui_tgt, email=email, client_id=client_id, tg_id=tg_id, new_expiry_time=new_expiry_time, total_gb=total_gb, hwid_device_limit=hwid_device_limit, attempt_update_first=was_on_xui_before, ) return client_id, remna_link