406 lines
16 KiB
Python
406 lines
16 KiB
Python
import asyncio
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from config import REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, SUPERNODE
|
|
from database import (
|
|
delete_notification,
|
|
filter_cluster_by_subgroup,
|
|
filter_cluster_by_tariff,
|
|
get_key_details,
|
|
get_servers,
|
|
get_tariff_by_id,
|
|
resolve_device_limit_from_group,
|
|
update_key_expiry,
|
|
update_key_link,
|
|
)
|
|
from handlers.utils import ALLOWED_GROUP_CODES
|
|
from hooks.processors import process_get_cryptolink_after_renewal
|
|
from logger import (
|
|
CLOGGER as logger,
|
|
PANEL_REMNA,
|
|
PANEL_XUI,
|
|
)
|
|
from panels._3xui import extend_client_key, get_xui_instance
|
|
from panels.remnawave import RemnawaveAPI
|
|
|
|
from .aggregated_links import make_aggregated_link
|
|
from ...tariffs.subgroup_migration import migrate_between_subgroups
|
|
|
|
|
|
async def resolve_cluster(session: AsyncSession, cluster_id: str):
|
|
"""Возвращает список серверов для кластера или конкретного сервера."""
|
|
servers = await get_servers(session)
|
|
cluster = servers.get(cluster_id)
|
|
if cluster:
|
|
return cluster
|
|
found = []
|
|
for _key, server_list in servers.items():
|
|
for s in server_list:
|
|
if s.get("server_name", "").lower() == cluster_id.lower():
|
|
found.append(s)
|
|
if found:
|
|
return found
|
|
raise ValueError(f"Кластер или сервер с ID/именем {cluster_id} не найден.")
|
|
|
|
|
|
async def renew_on_remnawave(
|
|
cluster: list,
|
|
client_id: str,
|
|
email: str,
|
|
tg_id: int,
|
|
new_expiry_time: int,
|
|
total_gb: int,
|
|
hwid_device_limit: int,
|
|
session: AsyncSession,
|
|
reset_traffic: bool,
|
|
target_server_name: str | None = None,
|
|
external_squad_uuid: str | None = None,
|
|
old_device_limit: int | None = None,
|
|
) -> bool:
|
|
remnawave_nodes = [
|
|
s for s in cluster if str(s.get("panel_type", "3x-ui")).lower() == "remnawave" and s.get("inbound_id")
|
|
]
|
|
if not remnawave_nodes:
|
|
return False
|
|
if target_server_name:
|
|
remnawave_nodes = [s for s in remnawave_nodes if s.get("server_name") == target_server_name] or remnawave_nodes[
|
|
:1
|
|
]
|
|
|
|
remna = RemnawaveAPI(remnawave_nodes[0]["api_url"])
|
|
if not await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD):
|
|
logger.error(f"{PANEL_REMNA} Не удалось войти в Remnawave API")
|
|
return False
|
|
hwid_device_limit = int(hwid_device_limit or 0)
|
|
|
|
if old_device_limit is not None and hwid_device_limit < old_device_limit:
|
|
try:
|
|
await remna.clear_all_hwid_devices(client_id)
|
|
logger.info(
|
|
f"{PANEL_REMNA} HWID устройства сброшены для {client_id} (лимит {old_device_limit} → {hwid_device_limit})"
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"{PANEL_REMNA} Ошибка сброса HWID: {e}")
|
|
|
|
expire_iso = datetime.utcfromtimestamp(new_expiry_time // 1000).isoformat() + "Z"
|
|
traffic_limit_bytes = total_gb * 1024 * 1024 * 1024 if total_gb else 0
|
|
active_inbounds = [s["inbound_id"] for s in remnawave_nodes]
|
|
|
|
update_kwargs = {
|
|
"uuid": client_id,
|
|
"expire_at": expire_iso,
|
|
"active_user_inbounds": active_inbounds,
|
|
"traffic_limit_bytes": traffic_limit_bytes,
|
|
"hwid_device_limit": hwid_device_limit,
|
|
"external_squad_uuid": external_squad_uuid,
|
|
}
|
|
|
|
updated = await remna.update_user(**update_kwargs)
|
|
if updated:
|
|
if reset_traffic:
|
|
try:
|
|
await remna.reset_user_traffic(client_id)
|
|
except Exception as e:
|
|
logger.warning(f"{PANEL_REMNA} reset_user_traffic: {e}")
|
|
logger.info(f"{PANEL_REMNA} Подписка {client_id} успешно продлена")
|
|
return True
|
|
logger.debug(f"{PANEL_REMNA} Не удалось продлить {client_id}. Автосоздание отключено.")
|
|
return False
|
|
|
|
|
|
async def renew_on_3xui(
|
|
cluster: list,
|
|
email: str,
|
|
client_id: str,
|
|
new_expiry_time: int,
|
|
total_gb: int,
|
|
hwid_device_limit: int,
|
|
tg_id: int,
|
|
update_links: bool = False,
|
|
target_server_name: str | None = None,
|
|
):
|
|
"""Продлевает подписку на 3x-ui серверах кластера."""
|
|
tasks = []
|
|
for server_info in cluster:
|
|
if target_server_name and server_info.get("server_name") != target_server_name:
|
|
continue
|
|
if str(server_info.get("panel_type", "3x-ui")).lower() != "3x-ui":
|
|
continue
|
|
inbound_id = server_info.get("inbound_id")
|
|
server_name = server_info.get("server_name", "unknown")
|
|
if not inbound_id:
|
|
logger.warning(f"{PANEL_XUI} INBOUND_ID отсутствует для сервера {server_name}. Пропуск.")
|
|
continue
|
|
if SUPERNODE:
|
|
unique_email = f"{email}_{server_name.lower()}"
|
|
sub_id_val = email
|
|
else:
|
|
unique_email = email
|
|
sub_id_val = unique_email
|
|
traffic_bytes = total_gb * 1024 * 1024 * 1024 if total_gb else 0
|
|
|
|
async def process_server(si, inbound, uniq, sub, name):
|
|
try:
|
|
xui = await get_xui_instance(si["api_url"])
|
|
except Exception as e:
|
|
logger.warning(f"{PANEL_XUI} [{name}] API недоступен: {e}")
|
|
return name, False, f"api_unavailable: {e}"
|
|
try:
|
|
updated = await extend_client_key(
|
|
xui=xui,
|
|
inbound_id=int(inbound),
|
|
email=uniq,
|
|
new_expiry_time=new_expiry_time,
|
|
client_id=client_id,
|
|
total_gb=traffic_bytes,
|
|
sub_id=sub,
|
|
tg_id=tg_id,
|
|
limit_ip=hwid_device_limit,
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"{PANEL_XUI} [{name}] ошибка продления: {e}")
|
|
updated = False
|
|
if updated:
|
|
return name, True, None
|
|
logger.debug(f"{PANEL_XUI} [{name}] не удалось обновить {uniq}. Автосоздание отключено.")
|
|
return name, False, "no_autocreate"
|
|
|
|
tasks.append(process_server(server_info, inbound_id, unique_email, sub_id_val, server_name))
|
|
|
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
failed = []
|
|
succeeded = []
|
|
for r in results:
|
|
if isinstance(r, Exception):
|
|
failed.append(("unknown", f"task_exception: {r}"))
|
|
continue
|
|
name, ok, err = r
|
|
if ok:
|
|
succeeded.append(name)
|
|
else:
|
|
failed.append((name, err or "unknown_error"))
|
|
if succeeded:
|
|
logger.info(f"{PANEL_XUI} продлено на: {', '.join(succeeded)}")
|
|
if failed:
|
|
logger.debug(f"{PANEL_XUI} не продлено на: " + ", ".join([f"{n} ({e})" for n, e in failed]))
|
|
return succeeded, failed
|
|
|
|
|
|
async def renew_key_in_cluster(
|
|
cluster_id: str,
|
|
email: str,
|
|
client_id: str,
|
|
new_expiry_time: int,
|
|
total_gb: int,
|
|
session: AsyncSession,
|
|
hwid_device_limit: int = 0,
|
|
reset_traffic: bool = True,
|
|
target_subgroup: str | None = None,
|
|
old_subgroup: str | None = None,
|
|
plan=None,
|
|
):
|
|
"""Продлевает ключ в кластере с учётом подгрупп и Remnawave/3x-ui."""
|
|
try:
|
|
servers_map = await get_servers(session)
|
|
|
|
kd = await get_key_details(session, email)
|
|
if not kd or kd.get("client_id") != client_id:
|
|
logger.error(f"Не найден ключ по email={email} и client_id={client_id}")
|
|
return False
|
|
|
|
tg_id = int(kd["tg_id"])
|
|
server_id = kd["server_id"]
|
|
|
|
old_device_limit = kd.get("current_device_limit") or kd.get("selected_device_limit")
|
|
if old_device_limit is None and kd.get("tariff_id"):
|
|
old_tariff = await get_tariff_by_id(session, kd["tariff_id"])
|
|
if old_tariff:
|
|
old_device_limit = old_tariff.get("device_limit")
|
|
|
|
single_server = None
|
|
if servers_map.get(server_id):
|
|
cluster = servers_map[server_id]
|
|
else:
|
|
for _k, sl in servers_map.items():
|
|
for s in sl:
|
|
if s.get("server_name") == server_id:
|
|
single_server = s
|
|
break
|
|
if single_server:
|
|
break
|
|
cluster = (
|
|
[single_server]
|
|
if single_server
|
|
else servers_map.get(cluster_id) or await resolve_cluster(session, cluster_id)
|
|
)
|
|
|
|
external_squad_uuid = None
|
|
if plan is not None:
|
|
tariff = await get_tariff_by_id(session, plan)
|
|
if tariff:
|
|
raw_external_squad = tariff.get("external_squad")
|
|
if raw_external_squad:
|
|
external_squad_uuid = raw_external_squad
|
|
else:
|
|
external_squad_uuid = ""
|
|
is_configurable = tariff.get("configurable", False)
|
|
if not is_configurable:
|
|
tariff_device_limit = tariff.get("device_limit")
|
|
if tariff_device_limit is not None:
|
|
hwid_device_limit = int(tariff_device_limit)
|
|
else:
|
|
dl = await resolve_device_limit_from_group(session, server_id)
|
|
if dl is not None:
|
|
hwid_device_limit = dl
|
|
|
|
if (target_subgroup or "") != (old_subgroup or "") and not single_server:
|
|
new_client_id, remna_link = await migrate_between_subgroups(
|
|
session=session,
|
|
cluster_all=cluster,
|
|
cluster_id=cluster_id,
|
|
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,
|
|
old_subgroup=old_subgroup,
|
|
target_subgroup=target_subgroup,
|
|
external_squad_uuid=external_squad_uuid,
|
|
tariff_id=plan,
|
|
)
|
|
|
|
await update_key_expiry(session, new_client_id or client_id, new_expiry_time)
|
|
for prefix in ["key_24h", "key_10h", "key_expired", "renew"]:
|
|
await delete_notification(session, tg_id, f"{email}_{prefix}")
|
|
|
|
try:
|
|
key_link = await make_aggregated_link(
|
|
session=session,
|
|
cluster_all=cluster,
|
|
cluster_id=cluster_id,
|
|
email=email,
|
|
client_id=new_client_id or client_id,
|
|
tg_id=tg_id,
|
|
subgroup_code=target_subgroup,
|
|
remna_link_override=remna_link,
|
|
plan=plan,
|
|
)
|
|
if key_link:
|
|
await update_key_link(session, email, key_link)
|
|
except Exception as le:
|
|
logger.warning(f"[Link] ошибка генерации/сохранения после миграции: {le}")
|
|
|
|
return True
|
|
|
|
if single_server:
|
|
cluster_scope = [single_server]
|
|
else:
|
|
if plan is not None:
|
|
filtered = await filter_cluster_by_tariff(session, cluster, plan, cluster_id)
|
|
if filtered is not cluster:
|
|
cluster_scope = filtered
|
|
elif target_subgroup:
|
|
cluster_scope = await filter_cluster_by_subgroup(
|
|
session, cluster, target_subgroup, cluster_id, tariff_id=plan
|
|
)
|
|
else:
|
|
cluster_scope = cluster
|
|
elif target_subgroup:
|
|
target = await filter_cluster_by_subgroup(session, cluster, target_subgroup, cluster_id, tariff_id=plan)
|
|
cluster_scope = target if target else cluster
|
|
else:
|
|
cluster_scope = cluster
|
|
|
|
if plan is not None:
|
|
tariff_for_filter = await get_tariff_by_id(session, plan)
|
|
if tariff_for_filter:
|
|
gc = (tariff_for_filter.get("group_code") or "").lower()
|
|
if gc in ALLOWED_GROUP_CODES:
|
|
bound_servers = [s for s in cluster_scope if gc in (s.get("special_groups") or [])]
|
|
if bound_servers:
|
|
cluster_scope = bound_servers
|
|
else:
|
|
logger.info(f"[Renewal] Нет серверов со спецгруппой '{gc}' в {cluster_id}")
|
|
|
|
if not cluster_scope:
|
|
logger.warning(f"[Renewal] Нет серверов после фильтрации в кластере {cluster_id}")
|
|
return False
|
|
|
|
remna_ok = await renew_on_remnawave(
|
|
cluster=cluster_scope,
|
|
client_id=client_id,
|
|
email=email,
|
|
tg_id=tg_id,
|
|
new_expiry_time=new_expiry_time,
|
|
total_gb=total_gb,
|
|
hwid_device_limit=hwid_device_limit,
|
|
session=session,
|
|
reset_traffic=reset_traffic,
|
|
target_server_name=server_id if single_server else None,
|
|
external_squad_uuid=external_squad_uuid,
|
|
old_device_limit=old_device_limit,
|
|
)
|
|
|
|
succeeded, _ = await renew_on_3xui(
|
|
cluster=cluster_scope,
|
|
email=email,
|
|
client_id=client_id,
|
|
new_expiry_time=new_expiry_time,
|
|
total_gb=total_gb,
|
|
hwid_device_limit=hwid_device_limit,
|
|
tg_id=tg_id,
|
|
update_links=False,
|
|
target_server_name=server_id if single_server else None,
|
|
)
|
|
|
|
if remna_ok or succeeded:
|
|
await update_key_expiry(session, client_id, new_expiry_time)
|
|
for prefix in ["key_24h", "key_10h", "key_expired", "renew"]:
|
|
await delete_notification(session, tg_id, f"{email}_{prefix}")
|
|
|
|
try:
|
|
remna_link_override = None
|
|
if remna_ok and cluster_scope:
|
|
remnawave_nodes = [
|
|
s
|
|
for s in cluster_scope
|
|
if str(s.get("panel_type", "3x-ui")).lower() == "remnawave" and s.get("inbound_id")
|
|
]
|
|
if remnawave_nodes:
|
|
remna_link_override = await process_get_cryptolink_after_renewal(
|
|
email=email,
|
|
cluster_id=cluster_id,
|
|
plan=plan,
|
|
session=session,
|
|
tg_id=tg_id,
|
|
remnawave_nodes=remnawave_nodes,
|
|
)
|
|
|
|
key_link = await make_aggregated_link(
|
|
session=session,
|
|
cluster_all=cluster_scope,
|
|
cluster_id=cluster_id,
|
|
email=email,
|
|
client_id=client_id,
|
|
tg_id=tg_id,
|
|
subgroup_code=target_subgroup,
|
|
remna_link_override=remna_link_override,
|
|
plan=plan,
|
|
)
|
|
if key_link:
|
|
await update_key_link(session, email, key_link)
|
|
except Exception as le:
|
|
logger.warning(f"[Link] ошибка генерации/сохранения после продления: {le}")
|
|
|
|
return True
|
|
|
|
return False
|
|
except Exception as e:
|
|
logger.error(f"Не удалось продлить ключ {client_id} в кластере/на сервере {cluster_id}: {e}")
|
|
raise
|