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: Строка с числом и склонённым словом "день/дня/дней".
"""