From 7bd5ecc55f91a2f61060a8bd47c095893e2646dc Mon Sep 17 00:00:00 2001 From: Vladless Date: Wed, 16 Apr 2025 22:50:19 +0300 Subject: [PATCH] remnawave in country_mode/save shortid/bug fixes --- bot.py | 2 +- cli_launcher.py | 41 ++++--- handlers/admin/clusters/clusters_handler.py | 94 +++++++--------- handlers/admin/clusters/keyboard.py | 12 +- handlers/admin/coupons/coupons_handler.py | 38 +++---- handlers/admin/coupons/keyboard.py | 8 +- handlers/admin/panel/panel_handler.py | 5 +- handlers/admin/servers/keyboard.py | 4 +- handlers/admin/servers/servers_handler.py | 41 +++---- handlers/admin/users/users_handler.py | 42 ++----- handlers/coupons.py | 32 +++--- handlers/instructions/instructions.py | 1 + handlers/keys/__init__.py | 18 +-- handlers/keys/key_connect.py | 16 +-- handlers/keys/key_freeze.py | 3 +- handlers/keys/key_mode/__init__.py | 3 +- handlers/keys/key_mode/key_cluster_mode.py | 24 ++-- handlers/keys/key_mode/key_country_mode.py | 106 ++++++++++++------ handlers/keys/key_mode/key_create.py | 7 +- handlers/keys/key_renew.py | 1 - handlers/keys/key_utils.py | 49 +++++--- handlers/keys/key_view.py | 21 ++-- handlers/keys/keys.py | 3 +- handlers/keys/subscriptions.py | 4 +- .../notifications/general_notifications.py | 14 +-- handlers/profile.py | 20 ++-- handlers/start.py | 25 +++-- handlers/utils.py | 5 +- 28 files changed, 310 insertions(+), 329 deletions(-) diff --git a/bot.py b/bot.py index 99121ab8..ed732638 100644 --- a/bot.py +++ b/bot.py @@ -19,7 +19,7 @@ bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTM storage = MemoryStorage() dp = Dispatcher(bot=bot, storage=storage) -version = "4.2-a140499" +version = "4.2-a160401" register_middleware(dp) diff --git a/cli_launcher.py b/cli_launcher.py index bee3d07b..5b9dc0da 100644 --- a/cli_launcher.py +++ b/cli_launcher.py @@ -1,18 +1,21 @@ import os -import sys +import re import subprocess +import sys + import requests + from rich.console import Console -from rich.prompt import Prompt, Confirm +from rich.prompt import Confirm, Prompt from rich.table import Table from rich.text import Text -import re from config import BOT_SERVICE + try: - sys.stdin.reconfigure(encoding='utf-8') - sys.stdout.reconfigure(encoding='utf-8') + sys.stdin.reconfigure(encoding="utf-8") + sys.stdout.reconfigure(encoding="utf-8") except Exception: pass @@ -30,14 +33,17 @@ console = Console() def print_logo(): - logo = Text(""" + logo = Text( + """ ███████╗ ██████╗ ██╗ ██████╗ ██████╗ ██████╗ ████████╗ ██╔════╝██╔═══██╗██║ ██╔═══██╗██╔══██╗██╔═══██╗╚══██╔══╝ ███████╗██║ ██║██║ ██║ ██║██████╔╝██║ ██║ ██║ ╚════██║██║ ██║██║ ██║ ██║██╔══██╗██║ ██║ ██║ ███████║╚██████╔╝███████╗╚██████╔╝██████╔╝╚██████╔╝ ██║ ╚══════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ -""", style="bold cyan") +""", + style="bold cyan", + ) console.print(logo) @@ -69,7 +75,9 @@ def install_dependencies(): console.print("[yellow]⚠️ Виртуальное окружение не найдено. Создаю...[/yellow]") subprocess.run("python3 -m venv venv", shell=True, check=True) - subprocess.run("bash -c 'source venv/bin/activate && pip install -r requirements.txt'", shell=True, check=True) + subprocess.run( + "bash -c 'source venv/bin/activate && pip install -r requirements.txt'", shell=True, check=True + ) except subprocess.CalledProcessError: console.print("[red]❌ Ошибка при установке зависимостей.[/red]") @@ -84,7 +92,7 @@ def get_local_version(): path = os.path.join(PROJECT_DIR, "bot.py") if not os.path.isfile(path): return None - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: for line in f: match = re.search(r'version\s*=\s*["\'](.+?)["\']', line) if match: @@ -133,7 +141,7 @@ def update_from_beta(): console.print("[red]❌ Ошибка при клонировании. Обновление отменено.[/red]") return - subprocess.run(f'rsync -a --exclude=img --exclude=handlers/buttons.py {TEMP_DIR}/ {PROJECT_DIR}/', shell=True) + subprocess.run(f"rsync -a --exclude=img --exclude=handlers/buttons.py {TEMP_DIR}/ {PROJECT_DIR}/", shell=True) subprocess.run(["rm", "-rf", TEMP_DIR]) install_dependencies() @@ -150,9 +158,7 @@ def update_from_release(): install_rsync_if_needed() try: - response = requests.get( - "https://api.github.com/repos/Vladless/Solo_bot/releases", timeout=10 - ) + response = requests.get("https://api.github.com/repos/Vladless/Solo_bot/releases", timeout=10) releases = response.json()[:3] tag_choices = [r["tag_name"] for r in releases] @@ -164,8 +170,7 @@ def update_from_release(): console.print(f"[cyan]{idx}.[/cyan] {tag}") selected = Prompt.ask( - "[bold blue]Выберите номер релиза[/bold blue]", - choices=[str(i) for i in range(1, len(tag_choices) + 1)] + "[bold blue]Выберите номер релиза[/bold blue]", choices=[str(i) for i in range(1, len(tag_choices) + 1)] ) tag_name = tag_choices[int(selected) - 1] @@ -173,7 +178,7 @@ def update_from_release(): subprocess.run(["rm", "-rf", TEMP_DIR]) subprocess.run(f"git clone --depth 1 --branch {tag_name} {GITHUB_REPO} {TEMP_DIR}", shell=True, check=True) - subprocess.run(f'rsync -a --exclude=img --exclude=handlers/buttons.py {TEMP_DIR}/ {PROJECT_DIR}/', shell=True) + subprocess.run(f"rsync -a --exclude=img --exclude=handlers/buttons.py {TEMP_DIR}/ {PROJECT_DIR}/", shell=True) subprocess.run(["rm", "-rf", TEMP_DIR]) install_dependencies() @@ -223,8 +228,8 @@ def main(): if os.geteuid() != 0: console.print("[bold red]⛔ Требуется запуск от имени root или через sudo.[/bold red]") sys.exit(1) - - os.chdir(PROJECT_DIR) + + os.chdir(PROJECT_DIR) print_logo() try: diff --git a/handlers/admin/clusters/clusters_handler.py b/handlers/admin/clusters/clusters_handler.py index c7dad32a..ae76c083 100644 --- a/handlers/admin/clusters/clusters_handler.py +++ b/handlers/admin/clusters/clusters_handler.py @@ -1,6 +1,7 @@ import asyncio import time +from datetime import datetime, timedelta from typing import Any import asyncpg @@ -10,14 +11,27 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, Message from py3xui import AsyncApi -from datetime import datetime, timedelta from backup import create_backup_and_send_to_admins -from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, TOTAL_GB, USE_COUNTRY_SELECTION, REMNAWAVE_PASSWORD, REMNAWAVE_LOGIN +from config import ( + ADMIN_PASSWORD, + ADMIN_USERNAME, + DATABASE_URL, + REMNAWAVE_LOGIN, + REMNAWAVE_PASSWORD, + TOTAL_GB, + USE_COUNTRY_SELECTION, +) from database import check_unique_server_name, get_servers, update_key_expiry from filters.admin import IsAdminFilter -from handlers.keys.key_utils import create_client_on_server, create_key_on_cluster, renew_key_in_cluster, delete_key_from_cluster +from handlers.keys.key_utils import ( + create_client_on_server, + create_key_on_cluster, + delete_key_from_cluster, + renew_key_in_cluster, +) from logger import logger +from panels.remnawave import RemnawaveAPI from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb from .keyboard import ( @@ -26,10 +40,9 @@ from .keyboard import ( build_cluster_management_kb, build_clusters_editor_kb, build_manage_cluster_kb, + build_panel_type_kb, build_sync_cluster_kb, - build_panel_type_kb ) -from panels.remnawave import RemnawaveAPI router = Router() @@ -194,7 +207,7 @@ async def handle_subscription_url_input(message: Message, state: FSMContext): await message.answer( text=f"Введите inbound_id для сервера {server_name} в кластере {cluster_name}:\n\n" - f"Для Remnawave это UUID Инбаунда, для 3x-ui — просто ID (например, 1).", + f"Для Remnawave это UUID Инбаунда, для 3x-ui — просто ID (например, 1).", reply_markup=build_admin_back_kb("clusters"), ) await state.set_state(AdminClusterStates.waiting_for_inbound_id) @@ -216,7 +229,9 @@ async def handle_inbound_id_input(message: Message, state: FSMContext): @router.callback_query(AdminClusterCallback.filter(F.action.in_(["panel_3xui", "panel_remnawave"])), IsAdminFilter()) -async def handle_panel_type_selection(callback_query: CallbackQuery, callback_data: AdminClusterCallback, state: FSMContext): +async def handle_panel_type_selection( + callback_query: CallbackQuery, callback_data: AdminClusterCallback, state: FSMContext +): panel_type = "3x-ui" if callback_data.action == "panel_3xui" else "remnawave" user_data = await state.get_data() @@ -437,7 +452,7 @@ async def handle_sync_cluster(callback_query: types.CallbackQuery, callback_data try: query_keys = """ - SELECT tg_id, client_id, email, expiry_time + SELECT tg_id, client_id, email, expiry_time, remnawave_link FROM keys WHERE server_id = $1 """ @@ -458,18 +473,17 @@ async def handle_sync_cluster(callback_query: types.CallbackQuery, callback_data await delete_key_from_cluster(cluster_name, key["email"], key["client_id"]) await session.execute( - "DELETE FROM keys WHERE tg_id = $1 AND client_id = $2", - key["tg_id"], - key["client_id"] + "DELETE FROM keys WHERE tg_id = $1 AND client_id = $2", key["tg_id"], key["client_id"] ) - result = await create_key_on_cluster( + await create_key_on_cluster( cluster_name, key["tg_id"], key["client_id"], key["email"], key["expiry_time"], session=session, + remnawave_link=key.get("remnawave_link"), ) await asyncio.sleep(0.5) @@ -477,7 +491,6 @@ async def handle_sync_cluster(callback_query: types.CallbackQuery, callback_data except Exception as e: logger.error(f"Ошибка при синхронизации ключа {key['client_id']} в {cluster_name}: {e}") - await callback_query.message.edit_text( text=f"✅ Ключи успешно синхронизированы для кластера {cluster_name}", reply_markup=build_admin_back_kb("clusters"), @@ -622,8 +635,7 @@ async def handle_new_cluster_name_input(message: Message, state: FSMContext, ses conn = await asyncpg.connect(DATABASE_URL) try: existing_cluster = await conn.fetchval( - "SELECT cluster_name FROM servers WHERE cluster_name = $1 LIMIT 1", - new_cluster_name + "SELECT cluster_name FROM servers WHERE cluster_name = $1 LIMIT 1", new_cluster_name ) if existing_cluster: await message.answer( @@ -632,23 +644,16 @@ async def handle_new_cluster_name_input(message: Message, state: FSMContext, ses ) return - keys_count = await conn.fetchval( - "SELECT COUNT(*) FROM keys WHERE server_id = $1", - old_cluster_name - ) + keys_count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE server_id = $1", old_cluster_name) async with conn.transaction(): await conn.execute( - "UPDATE servers SET cluster_name = $1 WHERE cluster_name = $2", - new_cluster_name, - old_cluster_name + "UPDATE servers SET cluster_name = $1 WHERE cluster_name = $2", new_cluster_name, old_cluster_name ) if keys_count > 0: await conn.execute( - "UPDATE keys SET server_id = $1 WHERE server_id = $2", - new_cluster_name, - old_cluster_name + "UPDATE keys SET server_id = $1 WHERE server_id = $2", new_cluster_name, old_cluster_name ) await message.answer( @@ -729,7 +734,7 @@ async def handle_new_server_name_input(message: Message, state: FSMContext, sess existing_server = await conn.fetchval( "SELECT server_name FROM servers WHERE cluster_name = $1 AND server_name = $2 LIMIT 1", cluster_name, - new_server_name + new_server_name, ) if existing_server: await message.answer( @@ -738,24 +743,19 @@ async def handle_new_server_name_input(message: Message, state: FSMContext, sess ) return - keys_count = await conn.fetchval( - "SELECT COUNT(*) FROM keys WHERE server_id = $1", - old_server_name - ) + keys_count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE server_id = $1", old_server_name) async with conn.transaction(): await conn.execute( "UPDATE servers SET server_name = $1 WHERE cluster_name = $2 AND server_name = $3", new_server_name, cluster_name, - old_server_name + old_server_name, ) if keys_count > 0: await conn.execute( - "UPDATE keys SET server_id = $1 WHERE server_id = $2", - new_server_name, - old_server_name + "UPDATE keys SET server_id = $1 WHERE server_id = $2", new_server_name, old_server_name ) final_text = f"✅ Название сервера успешно изменено с '{old_server_name}' на '{new_server_name}' в кластере '{cluster_name}'!" @@ -787,20 +787,14 @@ async def handle_server_transfer(callback_query: CallbackQuery, state: FSMContex conn = await asyncpg.connect(DATABASE_URL) try: async with conn.transaction(): - await conn.execute( - "UPDATE keys SET server_id = $1 WHERE server_id = $2", - new_server_name, - old_server_name - ) + await conn.execute("UPDATE keys SET server_id = $1 WHERE server_id = $2", new_server_name, old_server_name) await conn.execute( - "DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", - cluster_name, - old_server_name + "DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", cluster_name, old_server_name ) base_text = f"✅ Ключи успешно перенесены на сервер '{new_server_name}', сервер '{old_server_name}' удален!" - sync_reminder = "\n\n⚠️ Не забудьте сделать \"Синхронизацию\"." + sync_reminder = '\n\n⚠️ Не забудьте сделать "Синхронизацию".' final_text = base_text + (sync_reminder if USE_COUNTRY_SELECTION else "") await callback_query.message.edit_text( @@ -831,21 +825,13 @@ async def handle_cluster_transfer(callback_query: CallbackQuery, state: FSMConte conn = await asyncpg.connect(DATABASE_URL) try: async with conn.transaction(): + await conn.execute("UPDATE keys SET server_id = $1 WHERE server_id = $2", new_cluster_name, old_server_name) await conn.execute( - "UPDATE keys SET server_id = $1 WHERE server_id = $2", - new_cluster_name, - old_server_name - ) - await conn.execute( - "UPDATE keys SET server_id = $1 WHERE server_id = $2", - new_cluster_name, - old_cluster_name + "UPDATE keys SET server_id = $1 WHERE server_id = $2", new_cluster_name, old_cluster_name ) await conn.execute( - "DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", - cluster_name, - old_server_name + "DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", cluster_name, old_server_name ) await callback_query.message.edit_text( diff --git a/handlers/admin/clusters/keyboard.py b/handlers/admin/clusters/keyboard.py index b9042d5f..7d5e0247 100644 --- a/handlers/admin/clusters/keyboard.py +++ b/handlers/admin/clusters/keyboard.py @@ -1,5 +1,3 @@ -from typing import Optional - from aiogram.filters.callback_data import CallbackData from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder @@ -134,13 +132,7 @@ def build_sync_cluster_kb(cluster_servers: list, cluster_name: str) -> InlineKey def build_panel_type_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button( - text="🌐 3X-UI", - callback_data=AdminClusterCallback(action="panel_3xui").pack() - ) - builder.button( - text="🌀 Remnawave", - callback_data=AdminClusterCallback(action="panel_remnawave").pack() - ) + builder.button(text="🌐 3X-UI", callback_data=AdminClusterCallback(action="panel_3xui").pack()) + builder.button(text="🌀 Remnawave", callback_data=AdminClusterCallback(action="panel_remnawave").pack()) builder.row(build_admin_back_btn("clusters")) return builder.as_markup() diff --git a/handlers/admin/coupons/coupons_handler.py b/handlers/admin/coupons/coupons_handler.py index 78a598c5..2a5a8810 100644 --- a/handlers/admin/coupons/coupons_handler.py +++ b/handlers/admin/coupons/coupons_handler.py @@ -1,8 +1,10 @@ -from datetime import datetime import html -import pytz + +from datetime import datetime from typing import Any +import pytz + from aiogram import F, Router from aiogram.enums import ParseMode from aiogram.fsm.context import FSMContext @@ -217,18 +219,15 @@ async def handle_coupon_delete(callback_query: CallbackQuery, callback_data: Adm coupon_code = callback_data.coupon_code kb = InlineKeyboardBuilder() kb.button( - text="✅ Да, удалить", - callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code, confirm=True).pack() + text="✅ Да, удалить", callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code, confirm=True).pack() ) kb.button( - text="❌ Нет, отменить", - callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code, confirm=False).pack() + text="❌ Нет, отменить", callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code, confirm=False).pack() ) kb.adjust(1) await callback_query.message.edit_text( - f"Вы уверены, что хотите удалить купон {coupon_code}?", - reply_markup=kb.as_markup() + f"Вы уверены, что хотите удалить купон {coupon_code}?", reply_markup=kb.as_markup() ) @@ -242,15 +241,13 @@ async def confirm_coupon_delete(callback_query: CallbackQuery, callback_data: Ad result = await delete_coupon(coupon_code, session) if not result: await callback_query.message.edit_text( - f"❌ Купон с кодом {coupon_code} не найден.", - reply_markup=build_admin_back_kb("coupons") + f"❌ Купон с кодом {coupon_code} не найден.", reply_markup=build_admin_back_kb("coupons") ) return except Exception as e: logger.error(f"Ошибка при удалении купона: {e}") await callback_query.message.edit_text( - "Произошла ошибка при удалении купона.", - reply_markup=build_admin_back_kb("coupons") + "Произошла ошибка при удалении купона.", reply_markup=build_admin_back_kb("coupons") ) return @@ -295,7 +292,11 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any): return title = f"Купон {coupon['code']}" - description = f"Получи {coupon['amount']} рублей!" if coupon["amount"] > 0 else f"Продли подписку на {format_days(coupon['days'])}!" + description = ( + f"Получи {coupon['amount']} рублей!" + if coupon["amount"] > 0 + else f"Продли подписку на {format_days(coupon['days'])}!" + ) message_text = ( f"🎫 Купон: {coupon['code']}\n" f"{'💰 Бонус: ' + str(coupon['amount']) + ' рублей' if coupon['amount'] > 0 else '⏳ Продление: ' + format_days(coupon['days'])}\n" @@ -309,15 +310,8 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any): id=coupon_code, title=title, description=description, - input_message_content=InputTextMessageContent( - message_text=message_text, - parse_mode=ParseMode.HTML - ), + input_message_content=InputTextMessageContent(message_text=message_text, parse_mode=ParseMode.HTML), reply_markup=builder.as_markup(), ) - await inline_query.answer( - results=[result], - cache_time=86400, - is_personal=True - ) \ No newline at end of file + await inline_query.answer(results=[result], cache_time=86400, is_personal=True) diff --git a/handlers/admin/coupons/keyboard.py b/handlers/admin/coupons/keyboard.py index f497ec8e..3035fef3 100644 --- a/handlers/admin/coupons/keyboard.py +++ b/handlers/admin/coupons/keyboard.py @@ -12,7 +12,7 @@ from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn class AdminCouponDeleteCallback(CallbackData, prefix="admin_coupon_delete"): coupon_code: str - confirm: Optional[bool] = None + confirm: bool | None = None def build_coupons_kb() -> InlineKeyboardMarkup: @@ -59,7 +59,11 @@ def build_coupons_list_kb(coupons: list, current_page: int, total_pages: int) -> def format_coupons_list(coupons: list, username_bot: str) -> str: coupon_list = "📜 Список всех купонов:\n\n" for coupon in coupons: - value_text = f"💰 Сумма: {coupon['amount']} рублей" if coupon["amount"] > 0 else f"⏳ {format_days(coupon['days'])}" + value_text = ( + f"💰 Сумма: {coupon['amount']} рублей" + if coupon["amount"] > 0 + else f"⏳ {format_days(coupon['days'])}" + ) coupon_list += ( f"🏷️ Код: {coupon['code']}\n" f"{value_text}\n" diff --git a/handlers/admin/panel/panel_handler.py b/handlers/admin/panel/panel_handler.py index 1b72867d..38ca7b8e 100644 --- a/handlers/admin/panel/panel_handler.py +++ b/handlers/admin/panel/panel_handler.py @@ -1,16 +1,15 @@ from aiogram import F, Router +from aiogram.exceptions import TelegramBadRequest from aiogram.filters import Command from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, Message -from aiogram.exceptions import TelegramBadRequest from bot import version from filters.admin import IsAdminFilter +from logger import logger from .keyboard import AdminPanelCallback, build_panel_kb -from logger import logger - router = Router() diff --git a/handlers/admin/servers/keyboard.py b/handlers/admin/servers/keyboard.py index e74036ce..31d4c7d2 100644 --- a/handlers/admin/servers/keyboard.py +++ b/handlers/admin/servers/keyboard.py @@ -15,7 +15,9 @@ def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboar builder = InlineKeyboardBuilder() builder.button(text="🗑️ Удалить", callback_data=AdminServerCallback(action="delete", data=server_name).pack()) - builder.button(text="✏️ Сменить название", callback_data=AdminServerCallback(action="rename", data=server_name).pack()) + builder.button( + text="✏️ Сменить название", callback_data=AdminServerCallback(action="rename", data=server_name).pack() + ) builder.button(text=BACK, callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack()) builder.adjust(1) return builder.as_markup() diff --git a/handlers/admin/servers/servers_handler.py b/handlers/admin/servers/servers_handler.py index db23c632..816404fb 100644 --- a/handlers/admin/servers/servers_handler.py +++ b/handlers/admin/servers/servers_handler.py @@ -4,10 +4,10 @@ from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from handlers.buttons import BACK from database import delete_server, get_servers from filters.admin import IsAdminFilter +from handlers.buttons import BACK from ..panel.keyboard import build_admin_back_kb from .keyboard import ( @@ -73,10 +73,7 @@ async def process_callback_delete_server( ) return - keys_count = await session.fetchval( - "SELECT COUNT(*) FROM keys WHERE server_id = $1", - server_name - ) + keys_count = await session.fetchval("SELECT COUNT(*) FROM keys WHERE server_id = $1", server_name) if keys_count > 0: await state.update_data(server_name=server_name, cluster_name=cluster_name) @@ -87,7 +84,7 @@ async def process_callback_delete_server( FROM servers WHERE server_name != $1 """, - server_name + server_name, ) if all_servers: @@ -96,13 +93,12 @@ async def process_callback_delete_server( builder.row( InlineKeyboardButton( text=f"{server['server_name']} ({server['key_count']})", - callback_data=f"transfer_to_server|{server['server_name']}|{server_name}" + callback_data=f"transfer_to_server|{server['server_name']}|{server_name}", ) ) builder.row( InlineKeyboardButton( - text=BACK, - callback_data=AdminServerCallback(action="manage", data=server_name).pack() + text=BACK, callback_data=AdminServerCallback(action="manage", data=server_name).pack() ) ) @@ -114,22 +110,16 @@ async def process_callback_delete_server( return remaining_servers = await session.fetchval( - "SELECT COUNT(*) FROM servers WHERE cluster_name = $1 AND server_name != $2", - cluster_name, - server_name + "SELECT COUNT(*) FROM servers WHERE cluster_name = $1 AND server_name != $2", cluster_name, server_name ) if remaining_servers == 0: other_clusters = await session.fetch( - "SELECT DISTINCT cluster_name FROM servers WHERE cluster_name != $1", - cluster_name + "SELECT DISTINCT cluster_name FROM servers WHERE cluster_name != $1", cluster_name ) if other_clusters: - cluster_keys_count = await session.fetchval( - "SELECT COUNT(*) FROM keys WHERE server_id = $1", - cluster_name - ) + cluster_keys_count = await session.fetchval("SELECT COUNT(*) FROM keys WHERE server_id = $1", cluster_name) if cluster_keys_count > 0: await state.update_data(server_name=server_name, cluster_name=cluster_name) @@ -141,7 +131,7 @@ async def process_callback_delete_server( WHERE cluster_name != $1 GROUP BY cluster_name """, - cluster_name + cluster_name, ) builder = InlineKeyboardBuilder() @@ -149,13 +139,12 @@ async def process_callback_delete_server( builder.row( InlineKeyboardButton( text=f"{cluster['cluster_name']} ({cluster['key_count']})", - callback_data=f"transfer_to_cluster|{cluster['cluster_name']}|{cluster_name}|{server_name}" + callback_data=f"transfer_to_cluster|{cluster['cluster_name']}|{cluster_name}|{server_name}", ) ) builder.row( InlineKeyboardButton( - text=BACK, - callback_data=AdminServerCallback(action="manage", data=server_name).pack() + text=BACK, callback_data=AdminServerCallback(action="manage", data=server_name).pack() ) ) @@ -167,9 +156,7 @@ async def process_callback_delete_server( return await session.execute( - "DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", - cluster_name, - server_name + "DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", cluster_name, server_name ) await callback_query.message.edit_text( text=f"✅ Сервер '{server_name}' удален. Кластер '{cluster_name}' также удален, так как в нем не осталось серверов.", @@ -177,9 +164,7 @@ async def process_callback_delete_server( ) else: await session.execute( - "DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", - cluster_name, - server_name + "DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", cluster_name, server_name ) await callback_query.message.edit_text( text=f"✅ Сервер '{server_name}' удален.", diff --git a/handlers/admin/users/users_handler.py b/handlers/admin/users/users_handler.py index 67f34935..9e33017b 100644 --- a/handlers/admin/users/users_handler.py +++ b/handlers/admin/users/users_handler.py @@ -826,7 +826,9 @@ async def handle_users_export_referrals( @router.callback_query(AdminUserEditorCallback.filter(F.action == "users_create_key"), IsAdminFilter()) -async def handle_create_key_start(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any): +async def handle_create_key_start( + callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any +): tg_id = callback_data.tg_id await state.update_data(tg_id=tg_id) @@ -838,8 +840,7 @@ async def handle_create_key_start(callback_query: CallbackQuery, callback_data: if not countries: await callback_query.message.edit_text( - "❌ Нет доступных стран для создания ключа.", - reply_markup=build_editor_kb(tg_id) + "❌ Нет доступных стран для создания ключа.", reply_markup=build_editor_kb(tg_id) ) return @@ -850,8 +851,7 @@ async def handle_create_key_start(callback_query: CallbackQuery, callback_data: builder.row(build_admin_back_btn()) await callback_query.message.edit_text( - "🌍 Выберите страну для создания ключа:", - reply_markup=builder.as_markup() + "🌍 Выберите страну для создания ключа:", reply_markup=builder.as_markup() ) return @@ -862,8 +862,7 @@ async def handle_create_key_start(callback_query: CallbackQuery, callback_data: if not cluster_names: await callback_query.message.edit_text( - "❌ Нет доступных кластеров для создания ключа.", - reply_markup=build_editor_kb(tg_id) + "❌ Нет доступных кластеров для создания ключа.", reply_markup=build_editor_kb(tg_id) ) return @@ -873,8 +872,7 @@ async def handle_create_key_start(callback_query: CallbackQuery, callback_data: builder.row(build_admin_back_btn()) await callback_query.message.edit_text( - "🌐 Выберите кластер для создания ключа:", - reply_markup=builder.as_markup() + "🌐 Выберите кластер для создания ключа:", reply_markup=builder.as_markup() ) @@ -891,8 +889,7 @@ async def handle_create_key_country(callback_query: CallbackQuery, state: FSMCon builder.row(build_admin_back_btn()) await callback_query.message.edit_text( - text=f"🕒 Выберите срок действия ключа для страны {country}:", - reply_markup=builder.as_markup() + text=f"🕒 Выберите срок действия ключа для страны {country}:", reply_markup=builder.as_markup() ) @@ -928,15 +925,7 @@ async def handle_create_key_duration(callback_query: CallbackQuery, state: FSMCo if USE_COUNTRY_SELECTION and "country" in data: country = data["country"] - await create_key_on_cluster( - country, - tg_id, - client_id, - email, - expiry_ms, - plan=months, - session=session - ) + await create_key_on_cluster(country, tg_id, client_id, email, expiry_ms, plan=months, session=session) await state.clear() await callback_query.message.edit_text( @@ -946,15 +935,7 @@ async def handle_create_key_duration(callback_query: CallbackQuery, state: FSMCo elif "cluster_name" in data: cluster_name = data["cluster_name"] - await create_key_on_cluster( - cluster_name, - tg_id, - client_id, - email, - expiry_ms, - plan=months, - session=session - ) + await create_key_on_cluster(cluster_name, tg_id, client_id, email, expiry_ms, plan=months, session=session) await state.clear() await callback_query.message.edit_text( @@ -968,8 +949,7 @@ async def handle_create_key_duration(callback_query: CallbackQuery, state: FSMCo except Exception as e: logger.error(f"Ошибка при создании ключа: {e}") await callback_query.message.edit_text( - "❌ Не удалось создать ключ. Попробуйте позже.", - reply_markup=build_editor_kb(data.get("tg_id", 0)) + "❌ Не удалось создать ключ. Попробуйте позже.", reply_markup=build_editor_kb(data.get("tg_id", 0)) ) diff --git a/handlers/coupons.py b/handlers/coupons.py index fb4cc2f3..2d68010d 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -1,16 +1,20 @@ +import html + +from datetime import datetime from typing import Any +import pytz + from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder -import pytz -import html -from datetime import datetime from config import ADMIN_ID from database import ( + add_connection, + check_connection_exists, check_coupon_usage, create_coupon_usage, get_coupon_by_code, @@ -18,18 +22,16 @@ from database import ( update_balance, update_coupon_usage_count, update_key_expiry, - check_connection_exists, - add_connection, ) from handlers.buttons import MAIN_MENU from handlers.keys.key_utils import renew_key_in_cluster +from handlers.profile import process_callback_view_profile from handlers.texts import ( COUPON_ALREADY_USED_MSG, COUPON_INPUT_PROMPT, COUPON_NOT_FOUND_MSG, ) from handlers.utils import edit_or_send_message, format_days -from handlers.profile import process_callback_view_profile from logger import logger @@ -67,9 +69,7 @@ async def process_coupon_code(message: Message, state: FSMContext, session: Any) await activate_coupon(message, state, session, coupon_code=coupon_code) -async def activate_coupon( - message: Message, state: FSMContext, session: Any, coupon_code: str, admin: bool = False -): +async def activate_coupon(message: Message, state: FSMContext, session: Any, coupon_code: str, admin: bool = False): logger.info(f"Активация купона: {coupon_code}") coupon_record = await get_coupon_by_code(coupon_code, session) @@ -172,9 +172,7 @@ async def handle_key_extension(callback_query: CallbackQuery, state: FSMContext, return key = await session.fetchrow( - "SELECT * FROM keys WHERE tg_id = $1 AND client_id = $2", - callback_query.from_user.id, - client_id + "SELECT * FROM keys WHERE tg_id = $1 AND client_id = $2", callback_query.from_user.id, client_id ) if not key or key["is_frozen"]: await callback_query.message.edit_text("❌ Выбранная подписка не найдена или заморожена.") @@ -186,11 +184,7 @@ async def handle_key_extension(callback_query: CallbackQuery, state: FSMContext, new_expiry = max(now_ms, current_expiry) + (coupon["days"] * 86400 * 1000) await renew_key_in_cluster( - cluster_id=key["server_id"], - email=key["email"], - client_id=client_id, - new_expiry_time=new_expiry, - total_gb=0 + cluster_id=key["server_id"], email=key["email"], client_id=client_id, new_expiry_time=new_expiry, total_gb=0 ) await update_key_expiry(client_id, new_expiry, session) @@ -198,7 +192,9 @@ async def handle_key_extension(callback_query: CallbackQuery, state: FSMContext, await create_coupon_usage(coupon["id"], callback_query.from_user.id, session) alias = key.get("alias") or key["email"] - expiry_date = datetime.fromtimestamp(new_expiry / 1000, tz=pytz.timezone("Europe/Moscow")).strftime("%d.%m.%y, %H:%M") + expiry_date = datetime.fromtimestamp(new_expiry / 1000, tz=pytz.timezone("Europe/Moscow")).strftime( + "%d.%m.%y, %H:%M" + ) await callback_query.message.answer( f"✅ Купон активирован, подписка {alias} продлена на {format_days(coupon['days'])}⏳ до {expiry_date}📆." ) diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index 22545e69..c3fc3b90 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -1,4 +1,5 @@ import os + from typing import Any from aiogram import F, Router diff --git a/handlers/keys/__init__.py b/handlers/keys/__init__.py index fc75a083..a147f5de 100644 --- a/handlers/keys/__init__.py +++ b/handlers/keys/__init__.py @@ -2,20 +2,14 @@ __all__ = ("router",) from aiogram import Router -from .keys import router as keys_router -from .key_view import router as view_router -from .key_renew import router as renew_router -from .key_freeze import router as freeze_router from .key_connect import router as connect_router +from .key_freeze import router as freeze_router from .key_mode import router as key_mode_router +from .key_renew import router as renew_router +from .key_view import router as view_router +from .keys import router as keys_router + router = Router(name="keys_main_router") -router.include_routers( - keys_router, - view_router, - renew_router, - freeze_router, - connect_router, - key_mode_router -) +router.include_routers(keys_router, view_router, renew_router, freeze_router, connect_router, key_mode_router) diff --git a/handlers/keys/key_connect.py b/handlers/keys/key_connect.py index d6816342..3bc65985 100644 --- a/handlers/keys/key_connect.py +++ b/handlers/keys/key_connect.py @@ -1,4 +1,5 @@ import os + from io import BytesIO from typing import Any @@ -6,19 +7,10 @@ import asyncpg import qrcode from aiogram import F, Router, types - from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from config import ( - CONNECT_ANDROID, - CONNECT_IOS, - DATABASE_URL, - DOWNLOAD_ANDROID, - DOWNLOAD_IOS, - INSTRUCTIONS_BUTTON -) - +from config import CONNECT_ANDROID, CONNECT_IOS, DATABASE_URL, DOWNLOAD_ANDROID, DOWNLOAD_IOS, INSTRUCTIONS_BUTTON from handlers.buttons import ( ANDROID, BACK, @@ -108,7 +100,7 @@ async def process_callback_connect_phone(callback_query: CallbackQuery): InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{key_link}"), ) if INSTRUCTIONS_BUTTON: - builder.row(InlineKeyboardButton(text=MANUAL_INSTRUCTIONS, callback_data="instructions")) + builder.row(InlineKeyboardButton(text=MANUAL_INSTRUCTIONS, callback_data="instructions")) builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{email}")) await edit_or_send_message( @@ -234,4 +226,4 @@ async def show_qr_code(callback_query: types.CallbackQuery, session: Any): except Exception as e: logger.error(f"Ошибка при генерации QR: {e}", exc_info=True) - await callback_query.message.answer("❌ Произошла ошибка при создании QR-кода.") \ No newline at end of file + await callback_query.message.answer("❌ Произошла ошибка при создании QR-кода.") diff --git a/handlers/keys/key_freeze.py b/handlers/keys/key_freeze.py index 76c8fa5e..ca3bea85 100644 --- a/handlers/keys/key_freeze.py +++ b/handlers/keys/key_freeze.py @@ -3,7 +3,6 @@ import time from typing import Any from aiogram import F, Router - from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder @@ -216,4 +215,4 @@ async def process_callback_freeze_subscription_confirm(callback_query: CallbackQ ) except Exception as e: - await handle_error(tg_id, callback_query, f"Ошибка при заморозке подписки: {e}") \ No newline at end of file + await handle_error(tg_id, callback_query, f"Ошибка при заморозке подписки: {e}") diff --git a/handlers/keys/key_mode/__init__.py b/handlers/keys/key_mode/__init__.py index bcdd15ae..763fcfd7 100644 --- a/handlers/keys/key_mode/__init__.py +++ b/handlers/keys/key_mode/__init__.py @@ -2,9 +2,10 @@ __all__ = ("router",) from aiogram import Router -from .key_create import router as create_router from .key_cluster_mode import router as cluster_router from .key_country_mode import router as country_router +from .key_create import router as create_router + router = Router(name="key_mode_router") diff --git a/handlers/keys/key_mode/key_cluster_mode.py b/handlers/keys/key_mode/key_cluster_mode.py index 67575a52..91c8853f 100644 --- a/handlers/keys/key_mode/key_cluster_mode.py +++ b/handlers/keys/key_mode/key_cluster_mode.py @@ -5,7 +5,7 @@ from datetime import datetime import pytz from aiogram import Router -from aiogram.types import CallbackQuery, InlineKeyboardButton, Message, FSInputFile, WebAppInfo +from aiogram.types import CallbackQuery, FSInputFile, InlineKeyboardButton, Message, WebAppInfo from aiogram.utils.keyboard import InlineKeyboardBuilder from bot import bot @@ -20,19 +20,17 @@ from database import ( update_balance, update_trial, ) -from handlers.buttons import ( - CONNECT_DEVICE, - CONNECT_PHONE, - MAIN_MENU, - PC_BUTTON, - TV_BUTTON, - SUPPORT -) +from handlers.buttons import CONNECT_DEVICE, CONNECT_PHONE, MAIN_MENU, PC_BUTTON, SUPPORT, TV_BUTTON from handlers.keys.key_utils import create_key_on_cluster from handlers.texts import ( key_message_success, ) -from handlers.utils import edit_or_send_message, generate_random_email, get_least_loaded_cluster, is_full_remnawave_cluster +from handlers.utils import ( + edit_or_send_message, + generate_random_email, + get_least_loaded_cluster, + is_full_remnawave_cluster, +) from logger import logger @@ -63,9 +61,7 @@ async def key_cluster_mode( try: least_loaded_cluster = await get_least_loaded_cluster() - await create_key_on_cluster( - least_loaded_cluster, tg_id, client_id, email, expiry_timestamp, plan, session - ) + await create_key_on_cluster(least_loaded_cluster, tg_id, client_id, email, expiry_timestamp, plan, session) logger.info(f"[Key Creation] Ключ создан на кластере {least_loaded_cluster} для пользователя {tg_id}") key_record = await get_key_details(email, session) @@ -150,4 +146,4 @@ async def key_cluster_mode( ) if state: - await state.clear() \ No newline at end of file + await state.clear() diff --git a/handlers/keys/key_mode/key_country_mode.py b/handlers/keys/key_mode/key_country_mode.py index e019d070..3146008a 100644 --- a/handlers/keys/key_mode/key_country_mode.py +++ b/handlers/keys/key_mode/key_country_mode.py @@ -17,30 +17,21 @@ from config import ( ADMIN_USERNAME, CONNECT_PHONE_BUTTON, PUBLIC_LINK, + REMNAWAVE_LOGIN, + REMNAWAVE_PASSWORD, RENEWAL_PRICES, SUPPORT_CHAT_URL, - REMNAWAVE_LOGIN, - REMNAWAVE_PASSWORD ) from database import ( add_connection, check_connection_exists, + check_server_name_by_cluster, get_key_details, get_trial, update_balance, update_trial, - check_server_name_by_cluster, -) -from handlers.buttons import ( - BACK, - CONNECT_DEVICE, - CONNECT_PHONE, - MAIN_MENU, - PC_BUTTON, - SUPPORT, - TV_BUTTON, - SUPPORT ) +from handlers.buttons import BACK, CONNECT_DEVICE, CONNECT_PHONE, MAIN_MENU, PC_BUTTON, SUPPORT, TV_BUTTON from handlers.keys.key_utils import create_client_on_server from handlers.texts import ( SELECT_COUNTRY_MSG, @@ -48,8 +39,8 @@ from handlers.texts import ( ) from handlers.utils import edit_or_send_message, generate_random_email, get_least_loaded_cluster from logger import logger -from panels.three_xui import delete_client from panels.remnawave import RemnawaveAPI +from panels.three_xui import delete_client router = Router() @@ -73,7 +64,6 @@ async def key_country_mode( least_loaded_cluster, ) - if not servers: logger.error(f"Нет серверов в кластере {least_loaded_cluster}") error_message = "❌ Нет доступных серверов для создания ключа." @@ -88,7 +78,7 @@ async def key_country_mode( tasks = [asyncio.create_task(check_server_availability(server)) for server in servers] results = await asyncio.gather(*tasks, return_exceptions=True) - for server, result in zip(servers, results): + for server, result in zip(servers, results, strict=False): if result is True: available_servers.append(server["server_name"]) @@ -173,7 +163,7 @@ async def change_location_callback(callback_query: CallbackQuery, session: Any): results = await asyncio.gather(*tasks, return_exceptions=True) - for server, result in zip(servers, results): + for server, result in zip(servers, results, strict=False): if result is True: available_servers.append(server["server_name"]) @@ -266,6 +256,8 @@ async def finalize_key_creation( email = key_name.lower() expiry_timestamp = int(expiry_time.timestamp() * 1000) + remna = None + try: server_info = await session.fetchrow( "SELECT api_url, inbound_id, server_name, panel_type FROM servers WHERE server_name = $1", @@ -280,31 +272,53 @@ async def finalize_key_creation( remnawave_link = None created_at = int(datetime.now(moscow_tz).timestamp() * 1000) - if old_key_name and panel_type == "3x-ui": + if old_key_name: old_server_id = old_key_details.get("server_id") if old_server_id: old_server_info = await session.fetchrow( - "SELECT api_url, inbound_id, server_name FROM servers WHERE server_name = $1", + "SELECT api_url, inbound_id, server_name, panel_type FROM servers WHERE server_name = $1", old_server_id, ) if old_server_info: - xui = AsyncApi( - old_server_info["api_url"], - username=ADMIN_USERNAME, - password=ADMIN_PASSWORD, - logger=logger, - ) - await delete_client( - xui, - old_server_info["inbound_id"], - email, - client_id, - ) + old_panel_type = old_server_info["panel_type"].lower() + try: + if old_panel_type == "3x-ui": + xui = AsyncApi( + old_server_info["api_url"], + username=ADMIN_USERNAME, + password=ADMIN_PASSWORD, + logger=logger, + ) + await delete_client( + xui, + old_server_info["inbound_id"], + email, + client_id, + ) + await session.execute( + "UPDATE keys SET key = NULL WHERE tg_id = $1 AND email = $2", + tg_id, + email, + ) + logger.info(f"[Delete] Удалён клиент {email} с 3x-ui сервера {old_server_id}") + elif old_panel_type == "remnawave": + remna = RemnawaveAPI(old_server_info["api_url"]) + if await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD): + await remna.delete_user(client_id) + await session.execute( + "UPDATE keys SET remnawave_link = NULL WHERE tg_id = $1 AND email = $2", + tg_id, + email, + ) + logger.info(f"[Delete] Удалён клиент {client_id} с Remnawave сервера {old_server_id}") + else: + logger.warning(f"[Delete] Не удалось авторизоваться в Remnawave ({old_server_id})") + except Exception as e: + logger.warning(f"[Delete] Ошибка при удалении клиента с сервера {old_server_id}: {e}") if panel_type == "remnawave": remna = RemnawaveAPI(server_info["api_url"]) - logged_in = await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD) - if not logged_in: + if not await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD): raise ValueError(f"❌ Не удалось авторизоваться в Remnawave ({selected_country})") expire_at = datetime.utcfromtimestamp(expiry_timestamp / 1000).isoformat() + "Z" @@ -323,6 +337,14 @@ async def finalize_key_creation( remnawave_link = result.get("subscriptionUrl") logger.info(f"[Key Creation] Remnawave пользователь создан: {result}") + if old_key_name: + await session.execute( + "UPDATE keys SET client_id = $1 WHERE tg_id = $2 AND email = $3", + client_id, + tg_id, + email, + ) + if panel_type == "3x-ui": semaphore = asyncio.Semaphore(2) await create_client_on_server( @@ -344,6 +366,21 @@ async def finalize_key_creation( tg_id, old_key_name, ) + if panel_type == "3x-ui": + await session.execute( + "UPDATE keys SET key = $1 WHERE tg_id = $2 AND email = $3", + public_link, + tg_id, + email, + ) + elif panel_type == "remnawave": + await session.execute( + "UPDATE keys SET remnawave_link = $1 WHERE tg_id = $2 AND email = $3", + remnawave_link, + tg_id, + email, + ) + else: await session.execute( """ @@ -428,10 +465,9 @@ async def check_server_availability(server_info: dict) -> bool: logger.info(f"[Ping] 3x-ui сервер {server_name} доступен.") return True - except asyncio.TimeoutError: + except TimeoutError: logger.warning(f"[Ping] Сервер {server_name} не ответил вовремя.") return False except Exception as e: logger.warning(f"[Ping] Ошибка при проверке сервера {server_name}: {e}") return False - diff --git a/handlers/keys/key_mode/key_create.py b/handlers/keys/key_mode/key_create.py index 6a9ef0eb..31317dd6 100644 --- a/handlers/keys/key_mode/key_create.py +++ b/handlers/keys/key_mode/key_create.py @@ -7,8 +7,6 @@ from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder -from .key_cluster_mode import key_cluster_mode -from .key_country_mode import key_country_mode from config import ( NOTIFY_EXTRA_DAYS, @@ -40,6 +38,9 @@ from handlers.texts import ( from handlers.utils import edit_or_send_message from logger import logger +from .key_cluster_mode import key_cluster_mode +from .key_country_mode import key_country_mode + router = Router() @@ -199,4 +200,4 @@ async def create_key( session=session, message_or_query=message_or_query, plan=plan, - ) \ No newline at end of file + ) diff --git a/handlers/keys/key_renew.py b/handlers/keys/key_renew.py index a5225344..de6f5f8d 100644 --- a/handlers/keys/key_renew.py +++ b/handlers/keys/key_renew.py @@ -4,7 +4,6 @@ from typing import Any import asyncpg from aiogram import F, Router - from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index f60ab124..ae512e83 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -1,13 +1,24 @@ import asyncio +from datetime import datetime, timezone from typing import Any import asyncpg from py3xui import AsyncApi -from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD -from database import get_servers, store_key, delete_notification +from config import ( + ADMIN_PASSWORD, + ADMIN_USERNAME, + DATABASE_URL, + LIMIT_IP, + PUBLIC_LINK, + REMNAWAVE_LOGIN, + REMNAWAVE_PASSWORD, + SUPERNODE, + TOTAL_GB, +) +from database import delete_notification, get_servers, store_key from handlers.utils import get_least_loaded_cluster from logger import logger from panels.remnawave import RemnawaveAPI @@ -20,8 +31,6 @@ from panels.three_xui import ( toggle_client, ) -from datetime import datetime, timezone - async def create_key_on_cluster( cluster_id: str, @@ -31,6 +40,7 @@ async def create_key_on_cluster( expiry_timestamp: int, plan: int = None, session=None, + remnawave_link: str = None, ): try: servers = await get_servers() @@ -71,6 +81,9 @@ async def create_key_on_cluster( logger.warning("Нет inbound_id у серверов Remnawave") else: traffic_limit_bytes = int((plan or 1) * TOTAL_GB * 1024**3) + short_uuid = None + if remnawave_link and "/" in remnawave_link: + short_uuid = remnawave_link.rstrip("/").split("/")[-1] user_data = { "username": email, @@ -81,6 +94,9 @@ async def create_key_on_cluster( "activeUserInbounds": inbound_ids, } + if short_uuid: + user_data["shortUuid"] = short_uuid + result = await remna.create_user(user_data) if not result: logger.error("Ошибка при создании пользователя в Remnawave") @@ -243,8 +259,13 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to if remnawave_inbound_ids: remnawave_server = next( - (srv for srv in cluster if srv.get("panel_type", "").lower() == "remnawave" and srv.get("inbound_id") in remnawave_inbound_ids), - None + ( + srv + for srv in cluster + if srv.get("panel_type", "").lower() == "remnawave" + and srv.get("inbound_id") in remnawave_inbound_ids + ), + None, ) if not remnawave_server: @@ -258,7 +279,7 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to uuid=client_id, expire_at=expire_iso, active_user_inbounds=remnawave_inbound_ids, - traffic_limit_bytes=total_gb + traffic_limit_bytes=total_gb, ) if updated: logger.info(f"Подписка Remnawave {client_id} успешно продлена") @@ -426,9 +447,9 @@ async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id remnawave_key = result.get("subscriptionUrl") logger.info(f"[Update] Remnawave: клиент заново создан, новый UUID: {remnawave_client_id}") else: - logger.error(f"[Update] Ошибка создания Remnawave клиента") + logger.error("[Update] Ошибка создания Remnawave клиента") else: - logger.error(f"[Update] Не удалось авторизоваться в Remnawave") + logger.error("[Update] Не удалось авторизоваться в Remnawave") if not remnawave_client_id: logger.warning(f"[Update] Remnawave client_id не получен. Используется исходный: {client_id}") @@ -509,9 +530,7 @@ async def update_subscription(tg_id: int, email: str, session: Any, cluster_over ) new_cluster_id = cluster_override or await get_least_loaded_cluster() - new_client_id, remnawave_key = await update_key_on_cluster( - tg_id, client_id, email, expiry_time, new_cluster_id - ) + new_client_id, remnawave_key = await update_key_on_cluster(tg_id, client_id, email, expiry_time, new_cluster_id) servers = await get_servers() cluster_servers = servers.get(new_cluster_id, []) @@ -608,7 +627,9 @@ async def get_user_traffic(session: Any, tg_id: int, email: str) -> dict[str, An client_id = row["client_id"] server_id = row["server_id"] - matched_servers = [s for s in servers_map.values() if s["server_name"] == server_id or s["cluster_name"] == server_id] + matched_servers = [ + s for s in servers_map.values() if s["server_name"] == server_id or s["cluster_name"] == server_id + ] for server_info in matched_servers: tasks.append(fetch_traffic(server_info, client_id)) @@ -773,4 +794,4 @@ async def reset_traffic_in_cluster(cluster_id: str, email: str) -> None: except Exception as e: logger.error(f"[Reset Traffic] Ошибка при сбросе трафика клиента {email} в кластере {cluster_id}: {e}") - raise \ No newline at end of file + raise diff --git a/handlers/keys/key_view.py b/handlers/keys/key_view.py index 4794698c..8df665b2 100644 --- a/handlers/keys/key_view.py +++ b/handlers/keys/key_view.py @@ -1,15 +1,17 @@ -from aiogram import F, Router -from aiogram.types import CallbackQuery, Message, InlineKeyboardButton, WebAppInfo -from aiogram.utils.keyboard import InlineKeyboardBuilder -import pytz import html import os import re + from datetime import datetime -from aiogram.fsm.state import State, StatesGroup from typing import Any +import pytz + +from aiogram import F, Router from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from aiogram.types import CallbackQuery, InlineKeyboardButton, Message, WebAppInfo +from aiogram.utils.keyboard import InlineKeyboardBuilder from config import ( CONNECT_PHONE_BUTTON, @@ -52,6 +54,7 @@ from logger import logger router = Router() + class RenameKeyState(StatesGroup): waiting_for_new_alias = State() @@ -129,7 +132,7 @@ async def handle_rename_key(callback: CallbackQuery, state: FSMContext): await edit_or_send_message( target_message=callback.message, text="✏️ Введите новое имя подписки (до 10 символов):", - reply_markup=builder.as_markup() + reply_markup=builder.as_markup(), ) @@ -142,7 +145,9 @@ async def handle_new_alias_input(message: Message, state: FSMContext, session: A return if not alias or not re.match(r"^[a-zA-Zа-яА-ЯёЁ0-9@._-]+$", alias): - await message.answer("❌ Введены недопустимые символы или имя пустое. Используйте только буквы, цифры и @._-\nПовторите ввод.") + await message.answer( + "❌ Введены недопустимые символы или имя пустое. Используйте только буквы, цифры и @._-\nПовторите ввод." + ) return data = await state.get_data() @@ -303,4 +308,4 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any) tg_id, callback_query, f"Ошибка при получении информации о ключе: {e}", - ) \ No newline at end of file + ) diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index dabbcfa7..46c477e7 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -4,7 +4,6 @@ from typing import Any from aiogram import F, Router, types from aiogram.exceptions import TelegramBadRequest - from aiogram.types import CallbackQuery from database import ( @@ -17,11 +16,11 @@ from handlers.buttons import ( BACK, CANCEL, ) -from handlers.keys.key_view import process_callback_view_key from handlers.keys.key_utils import ( delete_key_from_cluster, update_subscription, ) +from handlers.keys.key_view import process_callback_view_key from handlers.texts import ( DELETE_KEY_CONFIRM_MSG, KEY_DELETED_MSG_SIMPLE, diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index f210a824..9628b36e 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -335,8 +335,8 @@ async def handle_subscription(request: web.Request, old_subscription: bool = Fal time_left = format_time_left(expiry_time_ms) urls = await get_subscription_urls( - server_id, email, conn, include_remnawave_key=client_data.get("remnawave_link") -) + server_id, email, conn, include_remnawave_key=client_data.get("remnawave_link") + ) if not urls: return web.Response(text="❌ Сервер не найден.", status=404) diff --git a/handlers/notifications/general_notifications.py b/handlers/notifications/general_notifications.py index 6c4c075d..77951edb 100644 --- a/handlers/notifications/general_notifications.py +++ b/handlers/notifications/general_notifications.py @@ -24,12 +24,12 @@ from database import ( add_notification, check_notification_time, delete_key, + delete_notification, get_all_keys, get_balance, get_last_notification_time, update_balance, update_key_expiry, - delete_notification ) from handlers.keys.key_utils import delete_key_from_cluster, renew_key_in_cluster from handlers.notifications.notify_kb import build_notification_expired_kb, build_notification_kb @@ -329,9 +329,7 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: keyboard, ) await add_notification(tg_id, notification_id, session=conn) - logger.info( - f"Отправлено уведомление о необходимости продления подписки {email} пользователю {tg_id}." - ) + logger.info(f"Отправлено уведомление о необходимости продления подписки {email} пользователю {tg_id}.") except Exception as e: logger.error(f"Не удалось отправить уведомление о продлении подписки пользователю {tg_id}: {e}") @@ -340,13 +338,7 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: async def process_auto_renew_or_notify( - bot, - conn, - key: dict, - notification_id: str, - renewal_period_months: int, - standard_photo: str, - standard_caption: str + bot, conn, key: dict, notification_id: str, renewal_period_months: int, standard_photo: str, standard_caption: str ): """ Если баланс пользователя позволяет, продлевает ключ на максимальный возможный срок и списывает средства; diff --git a/handlers/profile.py b/handlers/profile.py index a559db5c..98757337 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -1,12 +1,12 @@ import html -from io import BytesIO import os +from io import BytesIO +from typing import Any + import asyncpg import qrcode -from typing import Any - from aiogram import F, Router from aiogram.enums import ParseMode from aiogram.fsm.context import FSMContext @@ -50,7 +50,7 @@ from handlers.buttons import ( PAYMENT, QR, TOP_FIVE, - TRIAL_SUB + TRIAL_SUB, ) from handlers.texts import BALANCE_HISTORY_HEADER, BALANCE_MANAGEMENT_TEXT, INVITE_TEXT_NON_INLINE, TOP_REFERRALS_TEXT from logger import logger @@ -308,16 +308,16 @@ async def show_referral_qr(callback_query: CallbackQuery): except Exception as e: logger.error(f"Ошибка при генерации QR-кода для реферальной ссылки: {e}", exc_info=True) await callback_query.message.answer("❌ Произошла ошибка при создании QR-кода.") - + @router.callback_query(F.data == "top_referrals") async def top_referrals_handler(callback_query: CallbackQuery): conn = await asyncpg.connect(DATABASE_URL) try: - user_referral_count = await conn.fetchval( - "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", - callback_query.from_user.id - ) or 0 + user_referral_count = ( + await conn.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", callback_query.from_user.id) + or 0 + ) personal_block = "Твоё место в рейтинге:\n" if user_referral_count > 0: @@ -330,7 +330,7 @@ async def top_referrals_handler(callback_query: CallbackQuery): HAVING COUNT(*) > $1 ) AS better_users """, - user_referral_count + user_referral_count, ) personal_block += f"{user_position}. {callback_query.from_user.id} - {user_referral_count} чел." else: diff --git a/handlers/start.py b/handlers/start.py index 46333f95..ac0c3621 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -29,10 +29,10 @@ from database import ( check_connection_exists, get_referral_by_referred_id, get_trial, - update_balance, ) -from handlers.buttons import ABOUT_VPN, BACK, CHANNEL, MAIN_MENU, SUPPORT +from handlers.buttons import ABOUT_VPN, BACK, CHANNEL, MAIN_MENU, SUPPORT, TRIAL_SUB from handlers.captcha import generate_captcha +from handlers.coupons import activate_coupon from handlers.keys.key_mode.key_create import create_key from handlers.profile import process_callback_view_profile from handlers.texts import ( @@ -47,7 +47,6 @@ from handlers.texts import ( get_about_vpn, ) from logger import logger -from handlers.coupons import activate_coupon from .admin.panel.keyboard import AdminPanelCallback from .utils import edit_or_send_message @@ -69,13 +68,15 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin logger.info(f"Вызвана функция start_command для пользователя {message.chat.id}") if CAPTCHA_ENABLE and captcha: - captcha_data = await generate_captcha(message, state) - await edit_or_send_message( - target_message=message, - text=captcha_data["text"], - reply_markup=captcha_data["markup"], - ) - return + connection_exists = await check_connection_exists(message.chat.id) + if not connection_exists: + captcha_data = await generate_captcha(message, state) + await edit_or_send_message( + target_message=message, + text=captcha_data["text"], + reply_markup=captcha_data["markup"], + ) + return state_data = await state.get_data() text_to_process = state_data.get("original_text", message.text) @@ -269,7 +270,7 @@ async def show_start_menu(message: Message, admin: bool, session: Any): trial_status = await get_trial(message.chat.id, session) logger.info(f"Trial status для {message.chat.id}: {trial_status}") if trial_status == 0: - builder.row(InlineKeyboardButton(text="🎁 Пробная подписка", callback_data="create_key")) + builder.row(InlineKeyboardButton(text=TRIAL_SUB, callback_data="create_key")) else: logger.warning(f"Сессия базы данных отсутствует, пропускаем проверку триала для {message.chat.id}") @@ -321,4 +322,4 @@ async def handle_about_vpn(callback_query: CallbackQuery): reply_markup=builder.as_markup(), media_path=image_path, force_text=False, - ) \ No newline at end of file + ) diff --git a/handlers/utils.py b/handlers/utils.py index 71a677d8..17e64200 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -158,13 +158,14 @@ def get_plural_form(num: int, form1: str, form2: str, form3: str) -> str: return form3 return {1: form1, 2: form2, 3: form2, 4: form2}.get(n % 10, form3) + def format_days(days: int) -> str: """ Форматирует количество дней с правильным склонением. - + Args: days (int): Количество дней. - + Returns: str: Строка с числом и склонённым словом "день/дня/дней". """