diff --git a/bot.py b/bot.py index b8acd475..fae803d5 100644 --- a/bot.py +++ b/bot.py @@ -8,6 +8,7 @@ from aiogram.filters import ExceptionTypeFilter from aiogram.fsm.storage.memory import MemoryStorage from aiogram.types import BufferedInputFile, ErrorEvent from aiogram.utils.markdown import hbold +import subprocess from config import ADMIN_ID, API_TOKEN from filters.private import IsPrivateFilter @@ -17,7 +18,16 @@ bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTM storage = MemoryStorage() dp = Dispatcher(bot=bot, storage=storage) -version = "4.3-b240504 (ORM update)" + +def get_git_commit_number() -> str: + try: + count = subprocess.check_output(["git", "rev-list", "--count", "HEAD"]) + return count.decode("utf-8").strip() + except Exception: + return "unknown" + + +version = f"4.3-b240504 (commit #{get_git_commit_number()})" dp.message.filter(IsPrivateFilter()) dp.callback_query.filter(IsPrivateFilter()) diff --git a/database/keys.py b/database/keys.py index dae971e7..0a4048de 100644 --- a/database/keys.py +++ b/database/keys.py @@ -185,3 +185,13 @@ async def mark_key_as_unfrozen( ), {"expiry": new_expiry_time, "tg_id": tg_id, "client_id": client_id}, ) + + +async def update_key_tariff(session: AsyncSession, client_id: str, tariff_id: int): + await session.execute( + update(Key) + .where(Key.client_id == client_id) + .values(tariff_id=tariff_id) + ) + await session.commit() + logger.info(f"Тариф ключа {client_id} обновлён на {tariff_id}") diff --git a/database/tariffs.py b/database/tariffs.py index dd81fd5a..3d4aae07 100644 --- a/database/tariffs.py +++ b/database/tariffs.py @@ -45,6 +45,15 @@ async def get_tariffs_for_cluster(session: AsyncSession, cluster_name: str): .limit(1) ) row = server_row.first() + + if not row: + server_row = await session.execute( + select(Server.tariff_group) + .where(Server.server_name == cluster_name) + .limit(1) + ) + row = server_row.first() + if not row or not row[0]: return [] diff --git a/handlers/admin/clusters/clusters_handler.py b/handlers/admin/clusters/clusters_handler.py index f60f01f2..9b23e018 100644 --- a/handlers/admin/clusters/clusters_handler.py +++ b/handlers/admin/clusters/clusters_handler.py @@ -495,6 +495,7 @@ async def handle_sync_server( Key.client_id, Key.email, Key.expiry_time, + Key.tariff_id, ) .join(Key, Server.cluster_name == Key.server_id) .where(Server.server_name == server_name) @@ -530,6 +531,8 @@ async def handle_sync_server( key["email"], key["expiry_time"], semaphore, + plan=key["tariff_id"], + session=session, ) await asyncio.sleep(0.6) except Exception as e: diff --git a/handlers/admin/tariffs/tariffs_handler.py b/handlers/admin/tariffs/tariffs_handler.py index 6fc486d4..70af9ff5 100644 --- a/handlers/admin/tariffs/tariffs_handler.py +++ b/handlers/admin/tariffs/tariffs_handler.py @@ -156,7 +156,7 @@ async def process_tariff_traffic(message: Message, state: FSMContext): ) return - await state.update_data(traffic_limit=traffic * 1024**3 if traffic > 0 else None) + await state.update_data(traffic_limit=traffic if traffic > 0 else None) await state.set_state(TariffCreateState.device_limit) await message.answer( "📱 Введите лимит устройств (HWID) для тарифа (например: 3, 0 — безлимит):", @@ -315,7 +315,7 @@ async def view_tariff( return traffic_text = ( - f"{tariff.traffic_limit // 1024**3} ГБ" if tariff.traffic_limit else "Безлимит" + f"{tariff.traffic_limit} ГБ" if tariff.traffic_limit else "Безлимит" ) device_text = ( f"{tariff.device_limit}" if tariff.device_limit is not None else "Безлимит" @@ -453,7 +453,7 @@ async def apply_edit(message: Message, state: FSMContext, session: AsyncSession) if num < 0: raise ValueError if field == "traffic_limit": - value = num * 1024**3 if num > 0 else None + value = num if num > 0 else None elif field == "device_limit": value = num if num > 0 else None else: diff --git a/handlers/admin/users/users_handler.py b/handlers/admin/users/users_handler.py index 2b6cb007..b0e99b17 100644 --- a/handlers/admin/users/users_handler.py +++ b/handlers/admin/users/users_handler.py @@ -917,7 +917,12 @@ async def confirm_admin_key_reissue( ) return - await update_subscription(tg_id, email, session, cluster_override=cluster_id) + result = await session.execute( + select(Key.remnawave_link).where(Key.email == email) + ) + remnawave_link = result.scalar_one_or_none() + + await update_subscription(tg_id, email, session, cluster_override=cluster_id, remnawave_link=remnawave_link) await handle_key_edit( callback_query, @@ -1149,58 +1154,51 @@ async def process_user_search( async def change_expiry_time( expiry_time: int, email: str, session: AsyncSession ) -> Exception | None: - result = await session.execute(select(Key.client_id).where(Key.email == email)) - client_id = result.scalar_one_or_none() - if client_id is None: + result = await session.execute(select(Key.client_id, Key.tariff_id, Key.server_id).where(Key.email == email)) + row = result.first() + if not row: return ValueError(f"User with email {email} was not found") - - result = await session.execute( - select(Key.server_id).where(Key.client_id == client_id) - ) - server_id = result.scalar_one_or_none() + + client_id, tariff_id, server_id = row if server_id is None: return ValueError(f"Key with client_id {client_id} was not found") - result = await session.execute( - select(Server.tariff_group) - .where(or_(Server.server_name == server_id, Server.cluster_name == server_id)) - .limit(1) + traffic_limit = 0 + device_limit = None + if tariff_id: + result = await session.execute( + select(Tariff.traffic_limit, Tariff.device_limit) + .where(Tariff.id == 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 None + + servers = await get_servers(session=session) + + if server_id in servers: + target_cluster = server_id + else: + target_cluster = None + for cluster_name, cluster_servers in servers.items(): + if any(s.get("server_name") == server_id for s in cluster_servers): + target_cluster = cluster_name + break + + if not target_cluster: + return ValueError(f"No suitable cluster found for server {server_id}") + + await renew_key_in_cluster( + cluster_id=target_cluster, + email=email, + client_id=client_id, + new_expiry_time=expiry_time, + total_gb=traffic_limit, + session=session, + hwid_device_limit=device_limit ) - tariff_group = result.scalar_one_or_none() - if not tariff_group: - return ValueError(f"Tariff group not found for server_id={server_id}") - - result = await session.execute( - select(Tariff.duration_days, Tariff.traffic_limit) - .where(Tariff.group_code == tariff_group, Tariff.is_active.is_(True)) - .order_by(Tariff.duration_days) - ) - tariffs = result.all() - if not tariffs: - return ValueError(f"No tariffs found for group {tariff_group}") - - added_days = max((expiry_time - int(time.time() * 1000)) / (1000 * 86400), 1) - closest_tariff = min(tariffs, key=lambda t: abs(t[0] - added_days)) - traffic_limit = closest_tariff[1] or 0 - - clusters = await get_servers(session=session) - - async def update_key_on_all_servers(): - tasks = [ - renew_key_in_cluster( - cluster_id=cluster_name, - email=email, - client_id=client_id, - new_expiry_time=expiry_time, - total_gb=traffic_limit, - session=session, - ) - for cluster_name in clusters - ] - await asyncio.gather(*tasks, return_exceptions=True) - - await update_key_on_all_servers() - + await update_key_expiry(session, client_id, expiry_time) return None diff --git a/handlers/keys/key_mode/key_cluster_mode.py b/handlers/keys/key_mode/key_cluster_mode.py index b6ac41d9..17a8831e 100644 --- a/handlers/keys/key_mode/key_cluster_mode.py +++ b/handlers/keys/key_mode/key_cluster_mode.py @@ -77,15 +77,18 @@ async def key_cluster_mode( is_trial = data.get("is_trial", False) device_limit = 0 - traffic_limit_bytes = None + traffic_limit_gb = None if is_trial: device_limit = TRIAL_CONFIG.get("hwid_limit", 1) - traffic_limit_bytes = int(TRIAL_CONFIG.get("traffic_limit_gb", 100) * 1024**3) + traffic_limit_gb = TRIAL_CONFIG.get("traffic_limit_gb", 100) elif plan: tariff = await get_tariff_by_id(session, plan) - if tariff and tariff.get("device_limit") is not None: - device_limit = int(tariff["device_limit"]) + if tariff: + if tariff.get("device_limit") is not None: + device_limit = int(tariff["device_limit"]) + if tariff.get("traffic_limit") is not None: + traffic_limit_gb = int(tariff["traffic_limit"]) least_loaded_cluster = await get_least_loaded_cluster(session) await create_key_on_cluster( @@ -97,7 +100,8 @@ async def key_cluster_mode( plan=plan, session=session, hwid_limit=device_limit, - traffic_limit_bytes=traffic_limit_bytes, + traffic_limit_bytes=traffic_limit_gb, + is_trial=is_trial, ) logger.info( diff --git a/handlers/keys/key_mode/key_country_mode.py b/handlers/keys/key_mode/key_country_mode.py index 99d410ff..f145b17b 100644 --- a/handlers/keys/key_mode/key_country_mode.py +++ b/handlers/keys/key_mode/key_country_mode.py @@ -41,6 +41,7 @@ from handlers.buttons import ( PC_BUTTON, SUPPORT, TV_BUTTON, + MY_SUB ) from handlers.keys.key_utils import create_client_on_server from handlers.texts import SELECT_COUNTRY_MSG, key_message_success @@ -349,7 +350,6 @@ async def finalize_key_creation( client_id = old_key_details["client_id"] email = old_key_details["email"] expiry_timestamp = old_key_details["expiry_time"] - else: while True: key_name = generate_random_email() @@ -360,6 +360,25 @@ async def finalize_key_creation( email = key_name.lower() expiry_timestamp = int(expiry_time.timestamp() * 1000) + traffic_limit_bytes = None + device_limit = 0 + data = await state.get_data() if state else {} + is_trial = data.get("is_trial", False) + + if is_trial: + from config import TRIAL_CONFIG + traffic_limit_bytes = int(TRIAL_CONFIG.get("traffic_limit_gb", 100)) * 1024**3 + device_limit = TRIAL_CONFIG.get("hwid_limit", 1) + elif data.get("tariff_id") or tariff_id: + tariff_id = data.get("tariff_id") or tariff_id + result = await session.execute(select(Tariff).where(Tariff.id == tariff_id)) + tariff = result.scalar_one_or_none() + if tariff: + if tariff.traffic_limit is not None: + traffic_limit_bytes = int(tariff.traffic_limit) + if tariff.device_limit is not None: + device_limit = int(tariff.device_limit) + public_link = None remnawave_link = None created_at = int(datetime.now(moscow_tz).timestamp() * 1000) @@ -373,31 +392,22 @@ async def finalize_key_creation( raise ValueError(f"Сервер {selected_country} не найден") panel_type = server_info.panel_type.lower() - cluster_info = await check_server_name_by_cluster( - session, server_info.server_name - ) + cluster_info = await check_server_name_by_cluster(session, server_info.server_name) if not cluster_info: raise ValueError(f"Кластер для сервера {server_info.server_name} не найден") - is_full_remnawave = await is_full_remnawave_cluster( - cluster_info["cluster_name"], session - ) + is_full_remnawave = await is_full_remnawave_cluster(cluster_info["cluster_name"], session) if old_key_name: old_server_id = old_key_details["server_id"] if old_server_id: - result = await session.execute( - select(Server).where(Server.server_name == old_server_id) - ) + result = await session.execute(select(Server).where(Server.server_name == old_server_id)) old_server_info = result.scalar_one_or_none() if old_server_info: try: if old_server_info.panel_type.lower() == "3x-ui": xui = await get_xui_instance(old_server_info.api_url) - await delete_client( - xui, old_server_info.inbound_id, email, client_id - ) - + await delete_client(xui, old_server_info.inbound_id, email, client_id) await session.execute( update(Key) .where(Key.tg_id == tg_id, Key.email == email) @@ -418,21 +428,21 @@ async def finalize_key_creation( if panel_type == "remnawave" or is_full_remnawave: remna = RemnawaveAPI(server_info.api_url) if not await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD): - raise ValueError( - f"❌ Не удалось авторизоваться в Remnawave ({server_info.server_name})" - ) + raise ValueError(f"❌ Не удалось авторизоваться в Remnawave ({server_info.server_name})") - expire_at = ( - datetime.utcfromtimestamp(expiry_timestamp / 1000).isoformat() + "Z" - ) + expire_at = datetime.utcfromtimestamp(expiry_timestamp / 1000).isoformat() + "Z" user_data = { "username": email, "trafficLimitStrategy": "NO_RESET", "expireAt": expire_at, "telegramId": tg_id, "activeUserInbounds": [server_info.inbound_id], - "hwidDeviceLimit": 0, } + if traffic_limit_bytes: + user_data["trafficLimitBytes"] = traffic_limit_bytes + if device_limit: + user_data["hwidDeviceLimit"] = device_limit + result = await remna.create_user(user_data) if not result: raise ValueError("❌ Ошибка при создании пользователя в Remnawave") @@ -442,9 +452,7 @@ async def finalize_key_creation( if old_key_name: await session.execute( - update(Key) - .where(Key.tg_id == tg_id, Key.email == email) - .values(client_id=client_id) + update(Key).where(Key.tg_id == tg_id, Key.email == email).values(client_id=client_id) ) if panel_type == "3x-ui": @@ -461,12 +469,13 @@ async def finalize_key_creation( email=email, expiry_timestamp=expiry_timestamp, semaphore=semaphore, + session=session, + plan=tariff_id, + is_trial=is_trial, ) public_link = f"{PUBLIC_LINK}{email}/{tg_id}" - logger.info( - f"[Key Creation] Подписка создана для пользователя {tg_id} на сервере {selected_country}" - ) + logger.info(f"[Key Creation] Подписка создана для пользователя {tg_id} на сервере {selected_country}") if old_key_name: update_data = {"server_id": selected_country} @@ -474,19 +483,8 @@ async def finalize_key_creation( update_data["key"] = public_link elif panel_type == "remnawave": update_data["remnawave_link"] = remnawave_link - - await session.execute( - update(Key) - .where(Key.tg_id == tg_id, Key.email == email) - .values(**update_data) - ) + await session.execute(update(Key).where(Key.tg_id == tg_id, Key.email == email).values(**update_data)) else: - data = {} - if state: - data = await state.get_data() - - tariff_id = data.get("tariff_id") or tariff_id - new_key = Key( tg_id=tg_id, client_id=client_id, @@ -496,19 +494,17 @@ async def finalize_key_creation( key=public_link, remnawave_link=remnawave_link, server_id=selected_country, - tariff_id=data.get("tariff_id"), + tariff_id=tariff_id, ) session.add(new_key) - data = await state.get_data() - if data.get("is_trial"): + if is_trial: trial_status = await get_trial(session, tg_id) if trial_status in [0, -1]: await update_trial(session, tg_id, 1) - if data.get("tariff_id"): - result = await session.execute( - select(Tariff.price_rub).where(Tariff.id == data["tariff_id"]) - ) + + if tariff_id: + result = await session.execute(select(Tariff.price_rub).where(Tariff.id == tariff_id)) row = result.scalar_one_or_none() if row: await update_balance(session, tg_id, -row) @@ -516,21 +512,13 @@ async def finalize_key_creation( await session.commit() except Exception as e: - logger.error( - f"[Key Finalize] Ошибка при создании ключа для пользователя {tg_id}: {e}" - ) - await callback_query.message.answer( - "❌ Произошла ошибка при создании подписки. Попробуйте снова." - ) + logger.error(f"[Key Finalize] Ошибка при создании ключа для пользователя {tg_id}: {e}") + await callback_query.message.answer("❌ Произошла ошибка при создании подписки. Попробуйте снова.") return builder = InlineKeyboardBuilder() - is_full_remnawave = await is_full_remnawave_cluster( - cluster_info["cluster_name"], session - ) - if (panel_type == "remnawave" or is_full_remnawave) and ( - public_link or remnawave_link - ): + is_full_remnawave = await is_full_remnawave_cluster(cluster_info["cluster_name"], session) + if (panel_type == "remnawave" or is_full_remnawave) and (public_link or remnawave_link): builder.row( InlineKeyboardButton( text=CONNECT_DEVICE, @@ -539,9 +527,7 @@ async def finalize_key_creation( ) elif CONNECT_PHONE_BUTTON: builder.row( - InlineKeyboardButton( - text=CONNECT_PHONE, callback_data=f"connect_phone|{key_name}" - ) + InlineKeyboardButton(text=CONNECT_PHONE, callback_data=f"connect_phone|{key_name}") ) builder.row( InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{email}"), @@ -549,15 +535,10 @@ async def finalize_key_creation( ) else: builder.row( - InlineKeyboardButton( - text=CONNECT_DEVICE, callback_data=f"connect_device|{key_name}" - ) + InlineKeyboardButton(text=CONNECT_DEVICE, callback_data=f"connect_device|{key_name}") ) - builder.row( - InlineKeyboardButton( - text="🔐 Моя подписка", callback_data=f"view_key|{key_name}" - ) - ) + + builder.row(InlineKeyboardButton(text=MY_SUB, callback_data=f"view_key|{key_name}")) builder.row(InlineKeyboardButton(text=SUPPORT, url=SUPPORT_CHAT_URL)) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) diff --git a/handlers/keys/key_mode/key_create.py b/handlers/keys/key_mode/key_create.py index 9a7b739d..a42d45a7 100644 --- a/handlers/keys/key_mode/key_create.py +++ b/handlers/keys/key_mode/key_create.py @@ -27,6 +27,7 @@ from handlers.buttons import MAIN_MENU, PAYMENT from handlers.payments.robokassa_pay import handle_custom_amount_input from handlers.payments.stars_pay import process_custom_amount_input_stars from handlers.payments.yookassa_pay import process_custom_amount_input +from handlers.payments.yoomoney_pay import process_custom_amount_input_yoomoney from handlers.texts import ( CREATING_CONNECTION_MSG, INSUFFICIENT_FUNDS_MSG, @@ -165,6 +166,8 @@ async def select_tariff_plan( await handle_custom_amount_input(callback_query, session) elif USE_NEW_PAYMENT_FLOW == "STARS": await process_custom_amount_input_stars(callback_query, session) + elif USE_NEW_PAYMENT_FLOW == "YOOMONEY": + await process_custom_amount_input_yoomoney(callback_query, session) else: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay")) diff --git a/handlers/keys/key_renew.py b/handlers/keys/key_renew.py index b493a6db..e242a3a9 100644 --- a/handlers/keys/key_renew.py +++ b/handlers/keys/key_renew.py @@ -4,7 +4,7 @@ from typing import Any from aiogram import F, Router from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from sqlalchemy import or_, select +from sqlalchemy import or_, select, update from sqlalchemy.ext.asyncio import AsyncSession from bot import bot @@ -19,12 +19,13 @@ from database import ( update_balance, update_key_expiry, ) -from database.models import Server +from database.models import Server, Key from handlers.buttons import BACK, MAIN_MENU, PAYMENT from handlers.keys.key_utils import renew_key_in_cluster from handlers.payments.robokassa_pay import handle_custom_amount_input from handlers.payments.stars_pay import process_custom_amount_input_stars from handlers.payments.yookassa_pay import process_custom_amount_input +from handlers.payments.yoomoney_pay import process_custom_amount_input_yoomoney from handlers.texts import ( INSUFFICIENT_FUNDS_RENEWAL_MSG, KEY_NOT_FOUND_MSG, @@ -191,6 +192,8 @@ async def process_callback_renew_plan(callback_query: CallbackQuery, session: An await handle_custom_amount_input(callback_query, session) elif USE_NEW_PAYMENT_FLOW == "STARS": await process_custom_amount_input_stars(callback_query, session) + elif USE_NEW_PAYMENT_FLOW == "YOOMONEY": + await process_custom_amount_input_yoomoney(callback_query, session) else: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay")) @@ -316,6 +319,11 @@ async def complete_key_renewal( ) await update_key_expiry(session, client_id, new_expiry_time) + await session.execute( + update(Key) + .where(Key.client_id == client_id) + .values(tariff_id=tariff_id) + ) await update_balance(session, tg_id, -cost) logger.info( diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index 42bff44e..d23e5468 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -5,7 +5,7 @@ from typing import Any from sqlalchemy import delete, select from sqlalchemy.ext.asyncio import AsyncSession -from config import PUBLIC_LINK, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, SUPERNODE +from config import PUBLIC_LINK, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, SUPERNODE, TRIAL_CONFIG from database import delete_notification, get_servers, get_tariff_by_id, store_key from database.models import Key, Server, Tariff from handlers.utils import check_server_key_limit, get_least_loaded_cluster @@ -32,7 +32,8 @@ async def create_key_on_cluster( session: AsyncSession = None, remnawave_link: str = None, hwid_limit: int = None, - traffic_limit_bytes: int = None, + traffic_limit_bytes: int = None, + is_trial: bool = False, ): try: servers = await get_servers(session, include_enabled=True) @@ -125,7 +126,7 @@ async def create_key_on_cluster( } if traffic_limit_bytes and traffic_limit_bytes > 0: - user_data["trafficLimitBytes"] = traffic_limit_bytes + user_data["trafficLimitBytes"] = traffic_limit_bytes * 1024 * 1024 * 1024 if short_uuid: user_data["shortUuid"] = short_uuid @@ -161,6 +162,7 @@ async def create_key_on_cluster( semaphore, plan=plan, session=session, + is_trial=is_trial, ) else: await asyncio.gather( @@ -174,6 +176,7 @@ async def create_key_on_cluster( semaphore, plan=plan, session=session, + is_trial=is_trial, ) for server in xui_servers ], @@ -207,12 +210,13 @@ async def create_client_on_server( semaphore: asyncio.Semaphore, plan: int = None, session=None, + is_trial: bool = False, ): """ - Создает клиента на указанном 3x-ui сервере с лимитом по тарифу (через ORM). + Создает клиента на указанном 3x-ui сервере с лимитом по тарифу или триалу. """ logger.info( - f"[Client] Вход в create_client_on_server: сервер={server_info.get('server_name')}, план={plan}" + f"[Client] Вход в create_client_on_server: сервер={server_info.get('server_name')}, план={plan}, is_trial={is_trial}" ) async with semaphore: @@ -221,9 +225,7 @@ async def create_client_on_server( server_name = server_info.get("server_name", "unknown") if not inbound_id: - logger.warning( - f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск." - ) + logger.warning(f"[Client] INBOUND_ID отсутствует для сервера {server_name}. Пропуск.") return if SUPERNODE: @@ -236,26 +238,24 @@ async def create_client_on_server( total_gb_value = 0 device_limit_value = None - if plan is not None: + if is_trial: + total_gb_value = TRIAL_CONFIG.get("traffic_limit_gb", 0) + device_limit_value = TRIAL_CONFIG.get("hwid_limit") + logger.info(f"[Trial] Используются параметры триала: {total_gb_value} GB, {device_limit_value} устройств") + elif plan is not None: tariff = await get_tariff_by_id(session, plan) logger.info(f"[Tariff Debug] Получен тариф: {tariff}") if not tariff: raise ValueError(f"Тариф с id={plan} не найден.") - total_gb_value = ( - int(tariff["traffic_limit"]) if tariff.get("traffic_limit") else 0 - ) - device_limit_value = ( - int(tariff["device_limit"]) - if tariff.get("device_limit") is not None - else None - ) + total_gb_value = int(tariff["traffic_limit"]) if tariff["traffic_limit"] else 0 + device_limit_value = int(tariff["device_limit"]) if tariff.get("device_limit") is not None else None try: logger.info( - f"[Client] Вызов add_client: email={unique_email}, client_id={client_id}, GB={total_gb_value}, Devices={device_limit_value}" + f"[Client] Вызов add_client: email={email}, client_id={client_id}, GB={total_gb_value}, Devices={device_limit_value}" ) - + traffic_limit_bytes = total_gb_value * 1024 * 1024 * 1024 await add_client( xui, ClientConfig( @@ -263,7 +263,7 @@ async def create_client_on_server( email=unique_email, tg_id=tg_id, limit_ip=device_limit_value, - total_gb=total_gb_value, + total_gb=traffic_limit_bytes, expiry_time=expiry_timestamp, enable=True, flow="xtls-rprx-vision", @@ -273,9 +273,7 @@ async def create_client_on_server( ) logger.info(f"[Client] Клиент успешно добавлен на сервер {server_name}") except Exception as e: - logger.error( - f"[Client Error] Не удалось создать клиента на {server_name}: {e}" - ) + logger.error(f"[Client Error] Не удалось создать клиента на {server_name}: {e}") if SUPERNODE: await asyncio.sleep(0.7) @@ -335,12 +333,6 @@ async def renew_key_in_cluster( if tariff and tariff.device_limit is not None: hwid_device_limit = int(tariff.device_limit) - 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} очищены при продлении.") - remnawave_inbound_ids = [] tasks = [] for server_info in cluster: @@ -366,11 +358,12 @@ async def renew_key_in_cluster( 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=total_gb, + traffic_limit_bytes=traffic_limit_bytes, hwid_device_limit=hwid_device_limit, ) if updated: @@ -404,6 +397,7 @@ async def renew_key_in_cluster( unique_email = email sub_id = unique_email + traffic_bytes = total_gb * 1024 * 1024 * 1024 if total_gb else 0 tasks.append( extend_client_key( xui=xui, @@ -411,7 +405,7 @@ async def renew_key_in_cluster( email=unique_email, new_expiry_time=new_expiry_time, client_id=client_id, - total_gb=total_gb, + total_gb=traffic_bytes, sub_id=sub_id, tg_id=tg_id, limit_ip=hwid_device_limit, @@ -420,6 +414,12 @@ async def renew_key_in_cluster( 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} очищены при продлении.") + except Exception as e: logger.error( f"Не удалось продлить ключ {client_id} в кластере/на сервере {cluster_id}: {e}" @@ -510,6 +510,9 @@ async def update_key_on_cluster( expiry_time: int, cluster_id: str, session: AsyncSession, + traffic_limit: int = None, + device_limit: int = None, + remnawave_link: str = None, ): """ Пересоздаёт ключ на всех серверах указанного кластера (или сервера, если передано имя). @@ -568,6 +571,11 @@ async def update_key_on_cluster( ) tariff = result.scalar_one_or_none() + short_uuid = None + if remnawave_link and "/" in remnawave_link: + short_uuid = remnawave_link.rstrip("/").split("/")[-1] + logger.info(f"[Update] Извлечен short_uuid из ссылки: {short_uuid}") + user_data = { "username": email, "trafficLimitStrategy": "NO_RESET", @@ -575,12 +583,13 @@ async def update_key_on_cluster( "telegramId": tg_id, "activeUserInbounds": inbound_ids, } - - if tariff: - 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) + if traffic_limit is not None: + user_data["trafficLimitBytes"] = traffic_limit + if device_limit is not None: + user_data["hwidDeviceLimit"] = device_limit + if short_uuid: + user_data["shortUuid"] = short_uuid + logger.info(f"[Update] Добавлен short_uuid в user_data: {short_uuid}") result = await remna.create_user(user_data) if result: @@ -628,20 +637,14 @@ async def update_key_on_cluster( ) tariff = result.scalar_one_or_none() - total_gb_bytes = ( - int(tariff.traffic_limit) if tariff and tariff.traffic_limit else 0 - ) - device_limit = ( - int(tariff.device_limit) - if tariff and tariff.device_limit is not None - else None - ) + total_gb_bytes = int(traffic_limit * 1024 ** 3) if traffic_limit else 0 + device_limit_value = device_limit if device_limit is not None else None config = ClientConfig( client_id=remnawave_client_id, email=unique_email, tg_id=tg_id, - limit_ip=device_limit, + limit_ip=device_limit_value, total_gb=total_gb_bytes, expiry_time=expiry_time, enable=True, @@ -673,6 +676,7 @@ async def update_subscription( session: AsyncSession, cluster_override: str = None, country_override: str = None, + remnawave_link: str = None, ) -> None: result = await session.execute( select(Key).where(Key.tg_id == tg_id, Key.email == email) @@ -685,8 +689,25 @@ async def update_subscription( expiry_time = record.expiry_time client_id = record.client_id old_cluster_id = record.server_id + tariff_id = record.tariff_id + remnawave_link = remnawave_link or record.remnawave_link public_link = f"{PUBLIC_LINK}{email}/{tg_id}" + traffic_limit = None + device_limit = None + if tariff_id: + result = await session.execute( + select(Tariff).where(Tariff.id == tariff_id, Tariff.is_active.is_(True)) + ) + tariff = result.scalar_one_or_none() + if tariff: + traffic_limit = int(tariff.traffic_limit) if tariff.traffic_limit is not None else None + device_limit = int(tariff.device_limit) if tariff.device_limit is not None else None + else: + logger.warning(f"[LOG] update_subscription: тариф с id={tariff_id} не найден!") + else: + logger.warning(f"[LOG] update_subscription: tariff_id отсутствует!") + await delete_key_from_cluster(old_cluster_id, email, client_id, session=session) await session.execute(delete(Key).where(Key.tg_id == tg_id, Key.email == email)) @@ -703,6 +724,9 @@ async def update_subscription( expiry_time=expiry_time, cluster_id=new_cluster_id, session=session, + traffic_limit=traffic_limit, + device_limit=device_limit, + remnawave_link=remnawave_link ) servers = await get_servers(session) @@ -720,6 +744,7 @@ async def update_subscription( key=final_key_link, remnawave_link=remnawave_key, server_id=new_cluster_id, + tariff_id=tariff_id, ) @@ -1012,4 +1037,4 @@ async def reset_traffic_in_cluster( logger.error( f"[Reset Traffic] Ошибка при сбросе трафика клиента {email} в кластере {cluster_id}: {e}" ) - raise + raise \ No newline at end of file diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 50b5cc36..cfca39ff 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -76,6 +76,7 @@ async def process_callback_confirm_delete( record = await get_key_details(session, email) if record: client_id = record["client_id"] + server_id = record["server_id"] response_message = KEY_DELETED_MSG_SIMPLE back_button = types.InlineKeyboardButton( text=BACK, callback_data="view_keys" @@ -90,21 +91,12 @@ async def process_callback_confirm_delete( reply_markup=keyboard, ) - servers = await get_servers(session) - - async def delete_key_from_servers(): - try: - tasks = [ - delete_key_from_cluster(cluster_id, email, client_id, session) - for cluster_id in servers - ] - await asyncio.gather(*tasks, return_exceptions=True) - except Exception as e: - logger.error( - f"Ошибка при удалении ключа {client_id} с серверов: {e}" - ) - - asyncio.create_task(delete_key_from_servers()) + try: + await delete_key_from_cluster(server_id, email, client_id, session) + except Exception as e: + logger.error( + f"Ошибка при удалении ключа {client_id} с сервера {server_id}: {e}" + ) else: response_message = "Ключ не найден или уже удален." diff --git a/handlers/notifications/general_notifications.py b/handlers/notifications/general_notifications.py index 7bcee1f3..8ba52116 100644 --- a/handlers/notifications/general_notifications.py +++ b/handlers/notifications/general_notifications.py @@ -597,4 +597,4 @@ async def process_auto_renew_or_notify( ) except Exception as e: - logger.error(f"❌ Ошибка в process_auto_renew_or_notify: {e}") + logger.error(f"❌ Ошибка в process_auto_renew_or_notify: {e}") \ No newline at end of file diff --git a/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so b/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so index 6916e266..b9fed8bd 100644 Binary files a/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so and b/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so differ diff --git a/handlers/payments/utils.cpython-312-x86_64-linux-gnu.so b/handlers/payments/utils.cpython-312-x86_64-linux-gnu.so index bf93bcff..2dfd817c 100644 Binary files a/handlers/payments/utils.cpython-312-x86_64-linux-gnu.so and b/handlers/payments/utils.cpython-312-x86_64-linux-gnu.so differ diff --git a/handlers/payments/yoomoney_pay.cpython-312-x86_64-linux-gnu.so b/handlers/payments/yoomoney_pay.cpython-312-x86_64-linux-gnu.so index 6b4514fa..0a47da51 100644 Binary files a/handlers/payments/yoomoney_pay.cpython-312-x86_64-linux-gnu.so and b/handlers/payments/yoomoney_pay.cpython-312-x86_64-linux-gnu.so differ