diff --git a/handlers/admin/clusters/cluster_sync.py b/handlers/admin/clusters/cluster_sync.py index 65fcf066..4c258380 100644 --- a/handlers/admin/clusters/cluster_sync.py +++ b/handlers/admin/clusters/cluster_sync.py @@ -12,7 +12,6 @@ from sqlalchemy.ext.asyncio import AsyncSession from config import ( ADMIN_PASSWORD, ADMIN_USERNAME, - HAPP_CRYPTOLINK, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, USE_COUNTRY_SELECTION, diff --git a/handlers/admin/users/users_hwid.py b/handlers/admin/users/users_hwid.py index 97c7d7f2..fbfa4e72 100644 --- a/handlers/admin/users/users_hwid.py +++ b/handlers/admin/users/users_hwid.py @@ -52,12 +52,53 @@ async def handle_hwid_menu( await callback_query.message.edit_text("❌ Ошибка авторизации в Remnawave.") return + user_info = await api.get_user_by_uuid(client_id) devices = await api.get_user_hwid_devices(client_id) - if not devices: - text = "💻 HWID устройства\n\n🔌 Нет привязанных устройств." + status_emoji = "🟢" + status_text = "Онлайн" + online_at_str = "—" + first_connected_str = "—" + last_node_uuid = "—" + + if not user_info: + status_emoji = "⚪️" + status_text = "Не найден" else: - text = f"💻 HWID устройства\n\nПривязано: {len(devices)}\n\n" + is_online = bool(user_info.get("isOnline")) + status_emoji = "🟢" if is_online else "⚪️" + status_text = "Онлайн" if is_online else "Офлайн" + + online_at = user_info.get("onlineAt") + if online_at: + online_at_str = online_at[:19].replace("T", " ") + + first_connected_at = user_info.get("firstConnectedAt") + if first_connected_at: + first_connected_str = first_connected_at[:19].replace("T", " ") + + last_node_uuid_val = user_info.get("lastConnectedNodeUuid") + if last_node_uuid_val: + last_node_uuid = last_node_uuid_val + + if not devices: + text = ( + "💻 HWID устройства\n\n" + f"{status_emoji} Статус: {status_text}\n" + f"└ 🕓 Онлайн был: {online_at_str}\n" + f"└ 🚀 Первое подключение: {first_connected_str}\n" + f"└ 🛰 Нода последнего подключения: {last_node_uuid}\n\n" + "🔌 Нет привязанных устройств." + ) + else: + text = ( + "💻 HWID устройства\n\n" + f"{status_emoji} Статус: {status_text}\n" + f"└ 🕓 Онлайн был: {online_at_str}\n" + f"└ 🚀 Первое подключение: {first_connected_str}\n" + f"└ 🛰 Нода последнего подключения: {last_node_uuid}\n\n" + f"🔗 Привязано устройств: {len(devices)}\n\n" + ) for idx, device in enumerate(devices, 1): created = device.get("createdAt", "")[:19].replace("T", " ") updated = device.get("updatedAt", "")[:19].replace("T", " ") diff --git a/handlers/keys/operations/aggregated_links.py b/handlers/keys/operations/aggregated_links.py index c4b7b093..9481c9b2 100644 --- a/handlers/keys/operations/aggregated_links.py +++ b/handlers/keys/operations/aggregated_links.py @@ -23,7 +23,7 @@ async def _is_vless_tariff(session: AsyncSession, email: str) -> bool: return is_plan_vless(tariff) -async def _try_build_remna_vless(servers: list, email: str) -> tuple[str | None, str | None]: +async def _try_build_remna_vless(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] @@ -31,33 +31,28 @@ async def _try_build_remna_vless(servers: list, email: str) -> tuple[str | None, ok = await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD) if not ok: logger.warning("[Remnawave] login failed") - return None, None + return None, None, None data = await remna.get_subscription_by_username(email) if not data: logger.warning("[Remnawave] by-username empty") - return None, None + return None, None, None - links = data.get("links") or [] - best = None - if links: - best = max(links, key=score_vless_url) - if score_vless_url(best) < 0: - best = None + sub_url = data.get("subscriptionUrl") or None + + best_vless = None + if sub_url and sub_url.lower().startswith("vless://"): + best_vless = sub_url happ_link = None - try: - happ = data.get("happ") or {} - if isinstance(happ, dict): - happ_link = happ.get("cryptoLink") or happ.get("link") - except Exception: - pass + if happ_cryptolink_enabled and sub_url: + try: + happ_link = await remna.encrypt_happ_crypto_link(sub_url) + except Exception as e: + logger.warning(f"[Remnawave] happ encrypt failed: {e}") + happ_link = None - if happ_cryptolink_enabled and happ_link: - return best, happ_link - - sub_url = data.get("subscriptionUrl") - return best, sub_url + return best_vless, sub_url, happ_link async def _try_build_3xui_vless(servers: list, email: str) -> str | None: @@ -144,7 +139,7 @@ async def make_aggregated_link( logger.info("[agg_link] choose 3x-ui VLESS") return xui_link if remna: - best_vless, sub_url = await _try_build_remna_vless(remna, email) + best_vless, sub_url, happ_link = await _try_build_remna_vless(remna, email) if best_vless: logger.info("[agg_link] choose Remnawave VLESS") return best_vless @@ -156,8 +151,11 @@ async def make_aggregated_link( 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") + logger.info("[agg_link] choose Remnawave subscriptionUrl (vless)") return sub_url logger.info("[agg_link] fallback base link") return f"{base}/{email}/{tg_id}" @@ -166,15 +164,15 @@ async def make_aggregated_link( if legacy_links_enabled: logger.info("[agg_link] LEGACY non-vless -> base link") return f"{base}/{email}/{tg_id}" - best_vless, sub_url = await _try_build_remna_vless(remna, email) + best_vless, sub_url, happ_link = await _try_build_remna_vless(remna, email) 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 - if sub_url and sub_url.startswith("happ://"): + if happ_link: logger.info("[agg_link] choose Remnawave cryptoLink (non-vless)") - return sub_url + return happ_link kd = await get_key_details(session, email) stored = kd.get("remnawave_link") if kd else None if stored: diff --git a/handlers/keys/operations/subgroup_migration.py b/handlers/keys/operations/subgroup_migration.py index cb8a1325..ac6b37ed 100644 --- a/handlers/keys/operations/subgroup_migration.py +++ b/handlers/keys/operations/subgroup_migration.py @@ -52,6 +52,20 @@ async def ensure_on_remnawave( 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( @@ -61,16 +75,30 @@ async def ensure_on_remnawave( traffic_limit_bytes=traffic_bytes, hwid_device_limit=hwid_device_limit, ) - if updated: - if reset_traffic: - try: - await api.reset_user_traffic(client_id) - except Exception: - pass - return client_id, None - except Exception: + 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 - return None, None async def do_create(): try: @@ -84,19 +112,18 @@ async def ensure_on_remnawave( } if traffic_bytes > 0: payload["trafficLimitBytes"] = traffic_bytes - payload["hwidDeviceLimit"] = hwid_device_limit created = await api.create_user(payload) - new_uuid = created.get("uuid") if isinstance(created, dict) else None - remna_link = None - if isinstance(created, dict): - if use_crypto_link: - remna_link = ( - created.get("happ", {}).get("cryptoLink") if isinstance(created.get("happ"), dict) else None - ) - if not remna_link: - remna_link = created.get("subscriptionUrl") + 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}") diff --git a/hooks/constants.py b/hooks/constants.py index c59d449e..699e01a3 100644 --- a/hooks/constants.py +++ b/hooks/constants.py @@ -1 +1 @@ -DEFAULT_HOOK_TIMEOUT = 2.0 \ No newline at end of file +DEFAULT_HOOK_TIMEOUT = 4.0 \ No newline at end of file diff --git a/panels/remnawave.cpython-312-x86_64-linux-gnu.so b/panels/remnawave.cpython-312-x86_64-linux-gnu.so index 73be5853..e4341e00 100644 Binary files a/panels/remnawave.cpython-312-x86_64-linux-gnu.so and b/panels/remnawave.cpython-312-x86_64-linux-gnu.so differ