240 lines
11 KiB
Python
240 lines
11 KiB
Python
import asyncio
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import select, update
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from config import REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, SUPERNODE
|
|
from database import delete_notification, get_servers
|
|
from database.models import Key, Server, Tariff
|
|
from database.notifications import clear_hot_lead_notifications
|
|
from logger import logger
|
|
from panels._3xui import ClientConfig, add_client, extend_client_key, get_xui_instance
|
|
from panels.remnawave import RemnawaveAPI
|
|
|
|
|
|
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,
|
|
):
|
|
try:
|
|
servers = await get_servers(session)
|
|
cluster = servers.get(cluster_id)
|
|
|
|
if not cluster:
|
|
found_servers = []
|
|
for _key, server_list in servers.items():
|
|
for server_info in server_list:
|
|
if server_info.get("server_name", "").lower() == cluster_id.lower():
|
|
found_servers.append(server_info)
|
|
if found_servers:
|
|
cluster = found_servers
|
|
else:
|
|
raise ValueError(f"Кластер или сервер с ID/именем {cluster_id} не найден.")
|
|
|
|
result = await session.execute(select(Key.tg_id, Key.server_id).where(Key.client_id == client_id).limit(1))
|
|
row = result.first()
|
|
if not row:
|
|
logger.error(f"Не найден пользователь с client_id={client_id} в таблице keys.")
|
|
return False
|
|
|
|
tg_id, server_id = row
|
|
|
|
result = await session.execute(select(Server.tariff_group).where(Server.server_name == server_id))
|
|
tariff_group_row = result.scalar_one_or_none()
|
|
|
|
if tariff_group_row:
|
|
result = await session.execute(
|
|
select(Tariff)
|
|
.where(Tariff.group_code == tariff_group_row, Tariff.is_active.is_(True))
|
|
.order_by(Tariff.duration_days.desc())
|
|
.limit(1)
|
|
)
|
|
tariff = result.scalar_one_or_none()
|
|
if tariff and tariff.device_limit is not None:
|
|
hwid_device_limit = int(tariff.device_limit)
|
|
|
|
remnawave_inbound_ids = []
|
|
tasks = []
|
|
for server_info in cluster:
|
|
if server_info.get("panel_type", "3x-ui").lower() == "remnawave":
|
|
inbound_id = server_info.get("inbound_id")
|
|
if inbound_id:
|
|
remnawave_inbound_ids.append(inbound_id)
|
|
|
|
if remnawave_inbound_ids:
|
|
remnawave_server = next(
|
|
(
|
|
s
|
|
for s in cluster
|
|
if s.get("panel_type", "").lower() == "remnawave" and s.get("inbound_id") in remnawave_inbound_ids
|
|
),
|
|
None,
|
|
)
|
|
if remnawave_server:
|
|
remna = RemnawaveAPI(remnawave_server["api_url"])
|
|
if await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD):
|
|
expire_iso = datetime.utcfromtimestamp(new_expiry_time // 1000).isoformat() + "Z"
|
|
traffic_limit_bytes = total_gb * 1024 * 1024 * 1024 if total_gb else 0
|
|
updated = await remna.update_user(
|
|
uuid=client_id,
|
|
expire_at=expire_iso,
|
|
active_user_inbounds=remnawave_inbound_ids,
|
|
traffic_limit_bytes=traffic_limit_bytes,
|
|
hwid_device_limit=hwid_device_limit,
|
|
)
|
|
if updated:
|
|
logger.info(f"Подписка Remnawave {client_id} успешно продлена")
|
|
if reset_traffic:
|
|
await remna.reset_user_traffic(client_id)
|
|
else:
|
|
logger.warning(f"Не удалось продлить подписку Remnawave {client_id}, пробуем создать")
|
|
result = await session.execute(
|
|
select(Key.remnawave_link, Key.key).where(Key.client_id == client_id)
|
|
)
|
|
row = result.one_or_none()
|
|
remnawave_link = row[0] if row else None
|
|
row[1] if row else None
|
|
|
|
user_data = {
|
|
"username": email,
|
|
"trafficLimitStrategy": "NO_RESET",
|
|
"expireAt": expire_iso,
|
|
"telegramId": tg_id,
|
|
"activeInternalSquads": remnawave_inbound_ids,
|
|
}
|
|
if remnawave_link and "/" in remnawave_link:
|
|
user_data["shortUuid"] = remnawave_link.rstrip("/").split("/")[-1]
|
|
if traffic_limit_bytes and traffic_limit_bytes > 0:
|
|
user_data["trafficLimitBytes"] = traffic_limit_bytes
|
|
if hwid_device_limit is not None:
|
|
user_data["hwidDeviceLimit"] = hwid_device_limit
|
|
|
|
result = await remna.create_user(user_data)
|
|
if result:
|
|
new_client_id = result.get("uuid")
|
|
new_remnawave_link = result.get("subscriptionUrl")
|
|
logger.info(f"Пользователь Remnawave {client_id} успешно создан")
|
|
|
|
await session.execute(
|
|
update(Key)
|
|
.where(Key.client_id == client_id)
|
|
.values(client_id=new_client_id, remnawave_link=new_remnawave_link)
|
|
)
|
|
await session.commit()
|
|
else:
|
|
logger.error(f"Не удалось создать пользователя Remnawave {client_id}")
|
|
else:
|
|
logger.error("Не удалось войти в Remnawave API")
|
|
|
|
tasks = []
|
|
|
|
for server_info in cluster:
|
|
if 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"INBOUND_ID отсутствует для сервера {server_name}. Пропуск.")
|
|
continue
|
|
|
|
if SUPERNODE:
|
|
unique_email = f"{email}_{server_name.lower()}"
|
|
sub_id = email
|
|
else:
|
|
unique_email = email
|
|
sub_id = unique_email
|
|
|
|
traffic_bytes = total_gb * 1024 * 1024 * 1024 if total_gb else 0
|
|
|
|
async def process_server(server_info, inbound_id, unique_email, sub_id, server_name):
|
|
try:
|
|
xui = await get_xui_instance(server_info["api_url"])
|
|
except Exception as e:
|
|
logger.warning(f"[{server_name}] недоступна панель 3x-ui: {e}")
|
|
return server_name, False, f"api_unavailable: {e}"
|
|
|
|
try:
|
|
updated = await extend_client_key(
|
|
xui=xui,
|
|
inbound_id=int(inbound_id),
|
|
email=unique_email,
|
|
new_expiry_time=new_expiry_time,
|
|
client_id=client_id,
|
|
total_gb=traffic_bytes,
|
|
sub_id=sub_id,
|
|
tg_id=tg_id,
|
|
limit_ip=hwid_device_limit,
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"[{server_name}] ошибка при продлении: {e}")
|
|
updated = False
|
|
|
|
if updated:
|
|
return server_name, True, None
|
|
|
|
logger.warning(f"[{server_name}] не удалось обновить {unique_email}, пробуем создать")
|
|
try:
|
|
config = ClientConfig(
|
|
client_id=client_id,
|
|
email=unique_email,
|
|
tg_id=tg_id,
|
|
limit_ip=hwid_device_limit if hwid_device_limit is not None else 0,
|
|
total_gb=traffic_bytes,
|
|
expiry_time=new_expiry_time,
|
|
enable=True,
|
|
flow="xtls-rprx-vision",
|
|
inbound_id=int(inbound_id),
|
|
sub_id=sub_id,
|
|
)
|
|
await add_client(xui, config)
|
|
return server_name, True, None
|
|
except Exception as e:
|
|
logger.warning(f"[{server_name}] не удалось создать клиента: {e}")
|
|
return server_name, False, f"create_failed: {e}"
|
|
|
|
tasks.append(process_server(server_info, inbound_id, unique_email, sub_id, 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"3x-ui продлено на: {', '.join(succeeded)}")
|
|
if failed:
|
|
logger.warning("3x-ui не продлено на: " + ", ".join([f"{n} ({e})" for n, e in failed]))
|
|
|
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
notification_prefixes = ["key_24h", "key_10h", "key_expired", "renew"]
|
|
for notif in notification_prefixes:
|
|
notification_id = f"{email}_{notif}"
|
|
await delete_notification(session, tg_id, notification_id)
|
|
logger.info(f"🧹 Уведомления для ключа {email} очищены при продлении.")
|
|
|
|
try:
|
|
await clear_hot_lead_notifications(session, tg_id)
|
|
except Exception as e:
|
|
logger.warning(f"Не удалось очистить уведомления о скидках для {tg_id} при продлении: {e}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Не удалось продлить ключ {client_id} в кластере/на сервере {cluster_id}: {e}")
|
|
raise
|