Files
Solo_bot/services/tariffs/subgroup_migration.py
T

359 lines
12 KiB
Python

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
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]:
from panels.remnawave import RemnawaveAPI
from services.operations.utils import bytes_from_gb
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,
):
from services.operations.utils import bytes_from_gb
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]:
from services.operations.deletion import delete_on_3xui, delete_on_remnawave
from services.operations.utils import norm_name, split_by_panel
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