diff --git a/assets/schema.sql b/assets/schema.sql index 1150837c..bbed31cb 100644 --- a/assets/schema.sql +++ b/assets/schema.sql @@ -200,12 +200,15 @@ CREATE TABLE IF NOT EXISTS tariffs ( duration_days INTEGER NOT NULL CHECK (duration_days > 0), price_rub INTEGER NOT NULL CHECK (price_rub >= 0), traffic_limit BIGINT, + device_limit INTEGER, is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); ALTER TABLE servers ADD COLUMN IF NOT EXISTS tariff_group TEXT; +ALTER TABLE tariffs +ADD COLUMN IF NOT EXISTS device_limit INTEGER; DO $$ diff --git a/handlers/admin/tariffs/keyboard.py b/handlers/admin/tariffs/keyboard.py index 8178436e..c999e525 100644 --- a/handlers/admin/tariffs/keyboard.py +++ b/handlers/admin/tariffs/keyboard.py @@ -79,6 +79,7 @@ def build_edit_tariff_fields_kb(tariff_id: int) -> InlineKeyboardMarkup: [InlineKeyboardButton(text="📅 Длительность", callback_data=f"edit_field|{tariff_id}|duration_days")], [InlineKeyboardButton(text="💰 Цена", callback_data=f"edit_field|{tariff_id}|price_rub")], [InlineKeyboardButton(text="📦 Трафик (ГБ или 0)", callback_data=f"edit_field|{tariff_id}|traffic_limit")], + [InlineKeyboardButton(text="📱 Лимит устройств", callback_data=f"edit_field|{tariff_id}|device_limit")], [InlineKeyboardButton(text="🔘 Активность", callback_data=f"toggle_active|{tariff_id}")], [InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view|{tariff_id}")], ] diff --git a/handlers/admin/tariffs/tariffs_handler.py b/handlers/admin/tariffs/tariffs_handler.py index 0dd2722f..74856278 100644 --- a/handlers/admin/tariffs/tariffs_handler.py +++ b/handlers/admin/tariffs/tariffs_handler.py @@ -28,6 +28,7 @@ class TariffCreateState(StatesGroup): price = State() traffic = State() confirm_more = State() + device_limit = State() class TariffEditState(StatesGroup): @@ -115,7 +116,7 @@ async def process_tariff_price(message: Message, state: FSMContext): @router.message(TariffCreateState.traffic, IsAdminFilter()) -async def process_tariff_traffic(message: Message, state: FSMContext, session): +async def process_tariff_traffic(message: Message, state: FSMContext): try: traffic = int(message.text.strip()) if traffic < 0: @@ -124,8 +125,25 @@ async def process_tariff_traffic(message: Message, state: FSMContext, session): await message.answer("❌ Введите корректный лимит трафика (целое число 0 или больше):") return + await state.update_data(traffic_limit=traffic * 1024**3 if traffic > 0 else None) + await state.set_state(TariffCreateState.device_limit) + await message.answer( + "📱 Введите лимит устройств (HWID) для тарифа (например: 3, 0 — безлимит):", + reply_markup=build_cancel_kb(), + ) + + +@router.message(TariffCreateState.device_limit, IsAdminFilter()) +async def process_tariff_device_limit(message: Message, state: FSMContext, session): + try: + device_limit = int(message.text.strip()) + if device_limit < 0: + raise ValueError + except ValueError: + await message.answer("❌ Введите корректный лимит устройств (целое число 0 или больше):") + return + data = await state.get_data() - data["traffic_limit"] = traffic * 1024**3 if traffic > 0 else None new_tariff = await create_tariff( session, @@ -135,6 +153,7 @@ async def process_tariff_traffic(message: Message, state: FSMContext, session): "duration_days": data["duration_days"], "price_rub": data["price_rub"], "traffic_limit": data["traffic_limit"], + "device_limit": device_limit if device_limit > 0 else None, }, ) @@ -268,6 +287,7 @@ async def ask_new_value(callback: CallbackQuery, state: FSMContext): "duration_days": "длительность в днях", "price_rub": "цену в рублях", "traffic_limit": "лимит трафика в ГБ (0 — безлимит)", + "device_limit": "лимит устройств (0 — безлимит)", } await callback.message.edit_text( @@ -282,13 +302,15 @@ async def apply_edit(message: Message, state: FSMContext, session): field = data["field"] value = message.text.strip() - if field in ["duration_days", "price_rub", "traffic_limit"]: + if field in ["duration_days", "price_rub", "traffic_limit", "device_limit"]: try: num = int(value) if num < 0: raise ValueError if field == "traffic_limit": value = num * 1024**3 if num > 0 else None + elif field == "device_limit": + value = num if num > 0 else None else: value = num except ValueError: diff --git a/handlers/buttons.py b/handlers/buttons.py index d18c3f8e..5e97de69 100644 --- a/handlers/buttons.py +++ b/handlers/buttons.py @@ -10,7 +10,7 @@ CANCEL = "❌ Отмена" # Профиль -ADD_SUB = "➕ Подписка" +ADD_SUB = "➕ Добавить новую подписку" MY_SUBS = "📱 Мои подписки" BALANCE = "💰 Баланс" INVITE = "👥 Пригласить" diff --git a/handlers/keys/key_mode/key_cluster_mode.py b/handlers/keys/key_mode/key_cluster_mode.py index e26d0934..3dc206c5 100644 --- a/handlers/keys/key_mode/key_cluster_mode.py +++ b/handlers/keys/key_mode/key_cluster_mode.py @@ -9,7 +9,7 @@ from aiogram.types import CallbackQuery, FSInputFile, InlineKeyboardButton, Mess from aiogram.utils.keyboard import InlineKeyboardBuilder from bot import bot -from config import CONNECT_PHONE_BUTTON, DEFAULT_HWID_LIMIT, SUPPORT_CHAT_URL +from config import CONNECT_PHONE_BUTTON, SUPPORT_CHAT_URL from database import ( get_key_details, get_trial, @@ -56,6 +56,12 @@ async def key_cluster_mode( expiry_timestamp = int(expiry_time.timestamp() * 1000) try: + device_limit = 0 + if plan: + row = await session.fetchrow("SELECT device_limit FROM tariffs WHERE id = $1", plan) + if row and row["device_limit"] is not None: + device_limit = int(row["device_limit"]) + least_loaded_cluster = await get_least_loaded_cluster() await create_key_on_cluster( least_loaded_cluster, @@ -65,7 +71,7 @@ async def key_cluster_mode( expiry_timestamp, plan, session, - hwid_limit=DEFAULT_HWID_LIMIT, + hwid_limit=device_limit, ) logger.info(f"[Key Creation] Ключ создан на кластере {least_loaded_cluster} для пользователя {tg_id}") diff --git a/handlers/keys/key_mode/key_country_mode.py b/handlers/keys/key_mode/key_country_mode.py index ab8db0a7..c23aee8e 100644 --- a/handlers/keys/key_mode/key_country_mode.py +++ b/handlers/keys/key_mode/key_country_mode.py @@ -18,7 +18,6 @@ from config import ( ADMIN_USERNAME, CONNECT_PHONE_BUTTON, DATABASE_URL, - DEFAULT_HWID_LIMIT, PUBLIC_LINK, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, @@ -352,7 +351,7 @@ async def finalize_key_creation( "expireAt": expire_at, "telegramId": tg_id, "activeUserInbounds": [server_info["inbound_id"]], - "hwidDeviceLimit": DEFAULT_HWID_LIMIT, + "hwidDeviceLimit": 0, } result = await remna.create_user(user_data) if not result: diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index e9b73286..a0c035fb 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -7,8 +7,6 @@ import asyncpg from config import ( DATABASE_URL, - DEFAULT_HWID_LIMIT, - LIMIT_IP, PUBLIC_LINK, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, @@ -38,7 +36,7 @@ async def create_key_on_cluster( plan: int = None, session=None, remnawave_link: str = None, - hwid_limit: int | None = DEFAULT_HWID_LIMIT, + hwid_limit: int = None, ): try: servers = await get_servers(include_enabled=True) @@ -66,10 +64,12 @@ async def create_key_on_cluster( async with pool.acquire() as conn: traffic_limit_bytes = None if plan is not None: - tariff = await conn.fetchrow("SELECT traffic_limit FROM tariffs WHERE id = $1", plan) + tariff = await conn.fetchrow("SELECT traffic_limit, device_limit FROM tariffs WHERE id = $1", plan) if not tariff: raise ValueError(f"Тариф с id={plan} не найден.") - traffic_limit_bytes = int(tariff["traffic_limit"]) + traffic_limit_bytes = int(tariff["traffic_limit"]) if tariff["traffic_limit"] else None + hwid_limit = int(tariff["device_limit"]) if tariff["device_limit"] is not None else None + remnawave_servers = [ s @@ -199,13 +199,16 @@ async def create_client_on_server( sub_id = unique_email total_gb_value = 0 + device_limit_value = None + if plan is not None: async with asyncpg.create_pool(DATABASE_URL) as pool: async with pool.acquire() as conn: - tariff = await conn.fetchrow("SELECT traffic_limit FROM tariffs WHERE id = $1", plan) + tariff = await conn.fetchrow("SELECT traffic_limit, device_limit FROM tariffs WHERE id = $1", plan) if not tariff: raise ValueError(f"Тариф с id={plan} не найден.") - total_gb_value = int(tariff["traffic_limit"]) + total_gb_value = int(tariff["traffic_limit"]) if tariff["traffic_limit"] else 0 + device_limit_value = int(tariff["device_limit"]) if tariff["device_limit"] is not None else None await add_client( xui, @@ -213,7 +216,7 @@ async def create_client_on_server( client_id=client_id, email=unique_email, tg_id=tg_id, - limit_ip=LIMIT_IP, + limit_ip=device_limit_value, total_gb=total_gb_value, expiry_time=expiry_timestamp, enable=True, @@ -228,7 +231,7 @@ async def create_client_on_server( async def renew_key_in_cluster( - cluster_id, email, client_id, new_expiry_time, total_gb, hwid_device_limit=DEFAULT_HWID_LIMIT + cluster_id, email, client_id, new_expiry_time, total_gb, hwid_device_limit=None ): try: servers = await get_servers() @@ -247,14 +250,31 @@ async def renew_key_in_cluster( async with asyncpg.create_pool(DATABASE_URL) as pool: async with pool.acquire() as conn: - tg_id_query = "SELECT tg_id FROM keys WHERE client_id = $1 LIMIT 1" - tg_id_record = await conn.fetchrow(tg_id_query, client_id) + tg_id_record = await conn.fetchrow( + "SELECT tg_id, server_id FROM keys WHERE client_id = $1 LIMIT 1", client_id + ) if not tg_id_record: logger.error(f"Не найден пользователь с client_id={client_id} в таблице keys.") return False tg_id = tg_id_record["tg_id"] + server_id = tg_id_record["server_id"] + + tariff_group_row = await conn.fetchrow( + "SELECT tariff_group FROM servers WHERE server_name = $1", server_id + ) + if tariff_group_row and tariff_group_row["tariff_group"]: + tariff_row = await conn.fetchrow( + """ + SELECT device_limit FROM tariffs + WHERE group_code = $1 AND is_active = TRUE + ORDER BY duration_days DESC LIMIT 1 + """, + tariff_group_row["tariff_group"], + ) + if tariff_row and tariff_row["device_limit"] is not None: + hwid_device_limit = int(tariff_row["device_limit"]) notification_prefixes = ["key_24h", "key_10h", "key_expired", "renew"] for notif in notification_prefixes: @@ -298,7 +318,7 @@ async def renew_key_in_cluster( expire_at=expire_iso, active_user_inbounds=remnawave_inbound_ids, traffic_limit_bytes=total_gb, - hwid_device_limit=DEFAULT_HWID_LIMIT, + hwid_device_limit=hwid_device_limit, ) if updated: logger.info(f"Подписка Remnawave {client_id} успешно продлена") @@ -314,7 +334,6 @@ async def renew_key_in_cluster( if panel_type == "3x-ui": xui = await get_xui_instance(server_info["api_url"]) - inbound_id = server_info.get("inbound_id") if not inbound_id: @@ -457,11 +476,14 @@ async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id if group_id is None: raise ValueError("У Remnawave-сервера отсутствует tariff_group") tariff = await conn.fetchrow( - "SELECT traffic_limit FROM tariffs WHERE tariff_group = $1 ORDER BY traffic_limit_gb DESC LIMIT 1", + "SELECT traffic_limit, device_limit FROM tariffs WHERE group_code = $1 ORDER BY duration_days DESC LIMIT 1", group_id, ) if tariff: - user_data["trafficLimitBytes"] = int(tariff["traffic_limit"] * 1024**3) + if tariff["traffic_limit"] is not None: + user_data["trafficLimitBytes"] = int(tariff["traffic_limit"]) + if tariff["device_limit"] is not None: + user_data["hwidDeviceLimit"] = int(tariff["device_limit"]) result = await remna.create_user(user_data) if result: @@ -494,23 +516,25 @@ async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id unique_email = email total_gb_bytes = 0 + device_limit = None async with asyncpg.create_pool(DATABASE_URL) as pool: async with pool.acquire() as conn: group_id = server_info.get("tariff_group") if group_id is None: raise ValueError(f"У сервера {server_name} отсутствует tariff_group") tariff = await conn.fetchrow( - "SELECT traffic_limit FROM tariffs WHERE tariff_group = $1 ORDER BY traffic_limit_gb DESC LIMIT 1", + "SELECT traffic_limit, device_limit FROM tariffs WHERE group_code = $1 ORDER BY duration_days DESC LIMIT 1", group_id, ) if tariff: - total_gb_bytes = int(tariff["traffic_limit_gb"] * 1024**3) + total_gb_bytes = int(tariff["traffic_limit"]) if tariff["traffic_limit"] else 0 + device_limit = int(tariff["device_limit"]) if tariff["device_limit"] is not None else None config = ClientConfig( client_id=remnawave_client_id, email=unique_email, tg_id=tg_id, - limit_ip=LIMIT_IP, + limit_ip=device_limit, total_gb=total_gb_bytes, expiry_time=expiry_time, enable=True, diff --git a/handlers/profile.py b/handlers/profile.py index 133fe2aa..9e898b13 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -84,7 +84,7 @@ async def process_callback_view_profile( profile_message = profile_message_send(username, chat_id, int(balance), key_count) if key_count == 0: profile_message += ( - "\n
🔧 Нажмите кнопку ➕ Подписка, чтобы настроить VPN-подключение" + "\n
🔧 Нажмите кнопку ➕ Добавить новую подписку, чтобы настроить VPN-подключение" ) else: profile_message += f"\n
{NEWS_MESSAGE}" diff --git a/panels/three_xui.py b/panels/three_xui.py index b932ef86..571975c1 100644 --- a/panels/three_xui.py +++ b/panels/three_xui.py @@ -11,7 +11,6 @@ from py3xui import AsyncApi from config import ( ADMIN_PASSWORD, ADMIN_USERNAME, - LIMIT_IP, SUPERNODE, USE_XUI_TOKEN, XUI_TOKEN, @@ -122,7 +121,7 @@ async def extend_client_key( client.sub_id = sub_id client.total_gb = total_gb client.enable = True - client.limit_ip = LIMIT_IP + client.limit_ip client.inbound_id = inbound_id client.tg_id = tg_id @@ -213,7 +212,7 @@ async def toggle_client(xui: py3xui.AsyncApi, inbound_id: int, email: str, clien client.enable = enable client.id = client_id client.flow = "xtls-rprx-vision" - client.limit_ip = LIMIT_IP + client.limit_ip client.inbound_id = inbound_id await xui.client.update(client.id, client)