diff --git a/handlers/admin/clusters/cluster_manage.py b/handlers/admin/clusters/cluster_manage.py index 9c4fc540..89ee3c74 100644 --- a/handlers/admin/clusters/cluster_manage.py +++ b/handlers/admin/clusters/cluster_manage.py @@ -147,46 +147,99 @@ async def handle_days_input(message: Message, state: FSMContext, session: AsyncS await state.clear() return - for key in keys: - new_expiry = key.expiry_time + add_ms + servers = await get_servers(session=session) + cluster_servers = servers.get(cluster_name, []) + + if not cluster_servers: + await message.answer("❌ Не найдены серверы в кластере.") + await state.clear() + return - traffic_limit = 0 - device_limit = 0 - key_subgroup = None - if key.tariff_id: - result = await session.execute( - select(Tariff.traffic_limit, Tariff.device_limit, Tariff.subgroup_title).where( - Tariff.id == key.tariff_id, - Tariff.is_active.is_(True), - ) - ) - tariff = result.first() - if tariff: - traffic_limit = int(tariff[0]) if tariff[0] is not None else 0 - device_limit = int(tariff[1]) if tariff[1] is not None else 0 - key_subgroup = tariff[2] - - await renew_key_in_cluster( - cluster_name, - email=key.email, - client_id=key.client_id, - new_expiry_time=new_expiry, - total_gb=traffic_limit, - session=session, - hwid_device_limit=device_limit, - reset_traffic=False, - target_subgroup=key_subgroup, - old_subgroup=key_subgroup, - plan=key.tariff_id, - ) - await update_key_expiry(session, key.client_id, new_expiry) - - logger.info(f"[Cluster Extend] {key.email} +{days}д → {datetime.utcfromtimestamp(new_expiry / 1000)}") - - await message.answer( - f"✅ Время подписки продлено на {days} дней всем пользователям в кластере {cluster_name}." + is_full_remnawave = all( + str(s.get("panel_type", "")).lower() == "remnawave" + for s in cluster_servers ) + if is_full_remnawave: + uuids = [key.client_id for key in keys if key.client_id] + + if not uuids: + await message.answer("❌ Нет валидных подписок для продления.") + await state.clear() + return + + api_url = cluster_servers[0].get("api_url", "") + if not api_url: + await message.answer("❌ Не найден URL панели для кластера.") + await state.clear() + return + + from panels.remnawave import RemnawaveAPI + remna = RemnawaveAPI(api_url) + + try: + result_bulk = await remna.bulk_extend_expiration_date(uuids, days) + finally: + await remna.aclose() + + if result_bulk is None: + await message.answer("❌ Ошибка при обращении к API панели.") + await state.clear() + return + + affected = result_bulk.get("affectedRows", 0) + logger.info(f"[Cluster Extend] Bulk API: продлено {affected} подписок на {days} дней") + + for key in keys: + new_expiry = key.expiry_time + add_ms + await update_key_expiry(session, key.client_id, new_expiry) + + await session.commit() + + await message.answer( + f"✅ Время подписки продлено на {days} дней для {affected} пользователей в кластере {cluster_name}." + ) + else: + for key in keys: + new_expiry = key.expiry_time + add_ms + + traffic_limit = 0 + device_limit = 0 + key_subgroup = None + if key.tariff_id: + tariff_result = await session.execute( + select(Tariff.traffic_limit, Tariff.device_limit, Tariff.subgroup_title).where( + Tariff.id == key.tariff_id, + Tariff.is_active.is_(True), + ) + ) + tariff = tariff_result.first() + if tariff: + traffic_limit = int(tariff[0]) if tariff[0] is not None else 0 + device_limit = int(tariff[1]) if tariff[1] is not None else 0 + key_subgroup = tariff[2] + + await renew_key_in_cluster( + cluster_name, + email=key.email, + client_id=key.client_id, + new_expiry_time=new_expiry, + total_gb=traffic_limit, + session=session, + hwid_device_limit=device_limit, + reset_traffic=False, + target_subgroup=key_subgroup, + old_subgroup=key_subgroup, + plan=key.tariff_id, + ) + await update_key_expiry(session, key.client_id, new_expiry) + + logger.info(f"[Cluster Extend] {key.email} +{days}д → {datetime.utcfromtimestamp(new_expiry / 1000)}") + + await message.answer( + f"✅ Время подписки продлено на {days} дней всем пользователям в кластере {cluster_name}." + ) + except ValueError: await message.answer("❌ Введите корректное число дней.") except Exception as e: diff --git a/handlers/keys/operations/renewal.py b/handlers/keys/operations/renewal.py index 098282b5..71c3bcff 100644 --- a/handlers/keys/operations/renewal.py +++ b/handlers/keys/operations/renewal.py @@ -58,6 +58,7 @@ async def renew_on_remnawave( 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") @@ -74,6 +75,13 @@ async def renew_on_remnawave( logger.error(f"{PANEL_REMNA} Не удалось войти в Remnawave API") return False + 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] @@ -203,6 +211,12 @@ async def renew_key_in_cluster( 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] @@ -330,6 +344,7 @@ async def renew_key_in_cluster( 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( diff --git a/panels/remnawave.cpython-312-x86_64-linux-gnu.so b/panels/remnawave.cpython-312-x86_64-linux-gnu.so index 4dcb0b83..64db91e5 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