diff --git a/cli_launcher.py b/cli_launcher.py index a2312c87..a4eff79a 100644 --- a/cli_launcher.py +++ b/cli_launcher.py @@ -2,13 +2,17 @@ import os import re import subprocess import sys + import requests + from rich.console import Console from rich.prompt import Confirm, Prompt from rich.table import Table from rich.text import Text + from config import BOT_SERVICE + try: sys.stdin.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8") @@ -27,10 +31,12 @@ SERVICE_NAME = BOT_SERVICE console = Console() + def is_service_exists(service_name): result = subprocess.run(["systemctl", "list-unit-files", service_name], capture_output=True, text=True) return service_name in result.stdout + def print_logo(): logo = Text( """ @@ -45,6 +51,7 @@ def print_logo(): ) console.print(logo) + def backup_project(): console.print("[yellow]📦 Создаётся резервная копия проекта...[/yellow]") with console.status("[bold cyan]Копирование файлов...[/bold cyan]"): @@ -52,11 +59,13 @@ def backup_project(): subprocess.run(["cp", "-r", PROJECT_DIR, BACK_DIR]) console.print(f"[green]✅ Бэкап сохранён в: {BACK_DIR}[/green]") + def install_rsync_if_needed(): if subprocess.run(["which", "rsync"], capture_output=True).returncode != 0: console.print("[blue]📦 Установка rsync...[/blue]") os.system("sudo apt update && sudo apt install -y rsync") + def clean_project_dir_safe(): console.print("[yellow]🧹 Очистка проекта перед обновлением (кроме config и кнопок)...[/yellow]") preserved_paths = { @@ -92,6 +101,7 @@ def install_git_if_needed(): console.print("[blue]Установка Git...[/blue]") os.system("sudo apt update && sudo apt install -y git") + def install_dependencies(): console.print("[blue]🔧 Установка зависимостей...[/blue]") with console.status("[bold green]Устанавливаются зависимости...[/bold green]"): @@ -106,6 +116,7 @@ def install_dependencies(): except subprocess.CalledProcessError: console.print("[red]❌ Ошибка при установке зависимостей.[/red]") + def restart_service(): if is_service_exists(SERVICE_NAME): console.print("[blue]🚀 Перезапуск службы...[/blue]") @@ -114,6 +125,7 @@ def restart_service(): else: console.print(f"[red]❌ Служба {SERVICE_NAME} не найдена.[/red]") + def get_local_version(): path = os.path.join(PROJECT_DIR, "bot.py") if not os.path.isfile(path): @@ -125,6 +137,7 @@ def get_local_version(): return match.group(1) return None + def get_remote_version(branch="main"): try: url = f"https://raw.githubusercontent.com/Vladless/Solo_bot/{branch}/bot.py" @@ -138,6 +151,7 @@ def get_remote_version(branch="main"): return None return None + def update_from_beta(): local_version = get_local_version() remote_version = get_remote_version(branch="dev") @@ -173,6 +187,7 @@ def update_from_beta(): restart_service() console.print("[green]✅ Обновление с ветки dev завершено.[/green]") + def update_from_release(): if not Confirm.ask("[yellow]🔁 Подтвердите обновление Solobot до одного из последних релизов[/yellow]"): return @@ -194,8 +209,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] @@ -236,8 +250,9 @@ def show_update_menu(): elif choice == "2": update_from_release() + def show_menu(): - table = Table(title=f"Solobot CLI v0.1.4", title_style="bold magenta", header_style="bold blue") + table = Table(title="Solobot CLI v0.1.4", title_style="bold magenta", header_style="bold blue") table.add_column("№", justify="center", style="cyan", no_wrap=True) table.add_column("Операция", style="white") table.add_row("1", "Запустить бота (systemd)") @@ -250,6 +265,7 @@ def show_menu(): table.add_row("8", "Выход") console.print(table) + def main(): os.chdir(PROJECT_DIR) print_logo() @@ -295,5 +311,6 @@ def main(): except KeyboardInterrupt: console.print("\n[bold red]⏹ Прерывание. Выход из CLI.[/bold red]") + if __name__ == "__main__": main() diff --git a/database.py b/database.py index 9c0531dc..ca9cb90d 100644 --- a/database.py +++ b/database.py @@ -244,7 +244,13 @@ async def add_user( VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (tg_id) DO NOTHING """, - tg_id, username, first_name, last_name, language_code, is_bot, source_code + tg_id, + username, + first_name, + last_name, + language_code, + is_bot, + source_code, ) logger.info(f"[DB] Новый пользователь добавлен: {tg_id} (source: {source_code})") except Exception as e: @@ -1361,7 +1367,9 @@ async def get_all_keys(session: Any = None): await conn.close() -async def check_notifications_bulk(notification_type: str, hours: int, session: Any, tg_ids: list[int] = None, emails: list[str] = None) -> list[dict]: +async def check_notifications_bulk( + notification_type: str, hours: int, session: Any, tg_ids: list[int] = None, emails: list[str] = None +) -> list[dict]: """ Проверяет, какие пользователи могут получить уведомление указанного типа, и возвращает их данные. """ @@ -1380,25 +1388,25 @@ async def check_notifications_bulk(notification_type: str, hours: int, session: WHERE (n.last_notification_time IS NULL OR NOW() - n.last_notification_time > ($2 * INTERVAL '1 hour')) """ params = [notification_type, hours] - + if tg_ids is not None: query += " AND u.tg_id = ANY($3)" params.append(tg_ids) if emails is not None: query += " AND k.email = ANY($" + str(len(params) + 1) + ")" params.append(emails) - - if notification_type == 'inactive_trial': + + if notification_type == "inactive_trial": query += """ AND u.trial IN (0, -1) AND u.tg_id NOT IN (SELECT tg_id FROM blocked_users) AND u.tg_id NOT IN (SELECT DISTINCT tg_id FROM keys) """ - + query += """ GROUP BY u.tg_id, k.email, u.username, u.first_name, u.last_name """ - + users = await session.fetch(query, *params) logger.info(f"Найдено {len(users)} пользователей, готовых к уведомлению типа {notification_type}") return [ @@ -1408,7 +1416,9 @@ async def check_notifications_bulk(notification_type: str, hours: int, session: "username": user["username"], "first_name": user["first_name"], "last_name": user["last_name"], - "last_notification_time": int(user["last_notification_time"]) if user["last_notification_time"] else None, + "last_notification_time": int(user["last_notification_time"]) + if user["last_notification_time"] + else None, } for user in users ] @@ -1423,7 +1433,10 @@ async def create_tracking_source(name: str, code: str, type_: str, created_by: i INSERT INTO tracking_sources (name, code, type, created_by) VALUES ($1, $2, $3, $4) """, - name, code, type_, created_by + name, + code, + type_, + created_by, ) @@ -1446,7 +1459,8 @@ async def get_all_tracking_sources(session) -> list[dict]: async def get_tracking_source_stats(code: str, session) -> dict: - result = await session.fetchrow(""" + result = await session.fetchrow( + """ SELECT ts.name, ts.code, @@ -1463,5 +1477,7 @@ async def get_tracking_source_stats(code: str, session) -> dict: LEFT JOIN payments p ON p.tg_id = u.tg_id WHERE ts.code = $1 GROUP BY ts.code, ts.name, ts.created_at - """, code) + """, + code, + ) return dict(result) if result else {} diff --git a/handlers/__init__.py b/handlers/__init__.py index b2519c9d..de7b59b3 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -12,8 +12,8 @@ from .notifications import router as notifications_router from .pay import router as pay_router from .payments import router as payments_router from .profile import router as profile_router -from .start import router as start_router from .refferal import router as refferal_router +from .start import router as start_router router = Router(name="handlers_main_router") @@ -30,5 +30,5 @@ router.include_routers( keys_router, instructions_router, admin_router, - refferal_router + refferal_router, ) diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py index caff535a..94bf8ce5 100644 --- a/handlers/admin/__init__.py +++ b/handlers/admin/__init__.py @@ -2,6 +2,7 @@ __all__ = ("router",) from aiogram import Router +from .ads import router as ads_router from .backups import router as backups_router from .bans import router as bans_router from .clusters import router as clusters_router @@ -13,7 +14,6 @@ from .sender import router as sender_router from .servers import router as servers_router from .stats import router as stats_router from .users import router as users_router -from .ads import router as ads_router router = Router(name="admins_main_router") @@ -30,5 +30,5 @@ router.include_routers( coupons_router, restart_router, bans_router, - ads_router + ads_router, ) diff --git a/handlers/admin/ads/ads_handler.py b/handlers/admin/ads/ads_handler.py index 601b58bd..5c109364 100644 --- a/handlers/admin/ads/ads_handler.py +++ b/handlers/admin/ads/ads_handler.py @@ -16,13 +16,14 @@ from logger import logger from ..panel.keyboard import AdminPanelCallback from .keyboard import ( AdminAdsCallback, + build_ads_delete_confirm_kb, build_ads_kb, build_ads_list_kb, build_ads_stats_kb, - build_ads_delete_confirm_kb, - build_cancel_input_kb + build_cancel_input_kb, ) + router = Router() @@ -33,33 +34,27 @@ class AdminAdsState(StatesGroup): @router.callback_query(AdminPanelCallback.filter(F.action == "ads"), IsAdminFilter()) async def handle_ads_menu(callback_query: CallbackQuery): - await callback_query.message.edit_text( - text="📊 Аналитика рекламы:", reply_markup=build_ads_kb() - ) + await callback_query.message.edit_text(text="📊 Аналитика рекламы:", reply_markup=build_ads_kb()) @router.callback_query(AdminAdsCallback.filter(F.action == "create"), IsAdminFilter()) async def handle_ads_create(callback_query: CallbackQuery, state: FSMContext): await state.set_state(AdminAdsState.waiting_for_new_name) await callback_query.message.edit_text( - "📝 Введите название новой ссылки:", - reply_markup=build_cancel_input_kb() + "📝 Введите название новой ссылки:", reply_markup=build_cancel_input_kb() ) - @router.message(AdminAdsState.waiting_for_new_name, IsAdminFilter()) async def handle_ads_name_input(message: Message, state: FSMContext): name = message.text.strip() await state.update_data(name=name) await state.set_state(AdminAdsState.waiting_for_new_code) await message.answer( - f"🔗 Введите код ссылки для: {name}.", - reply_markup=build_cancel_input_kb() + f"🔗 Введите код ссылки для: {name}.", reply_markup=build_cancel_input_kb() ) - @router.message(AdminAdsState.waiting_for_new_code, IsAdminFilter()) async def handle_ads_code_input(message: Message, state: FSMContext, session): code = message.text.strip() @@ -69,11 +64,7 @@ async def handle_ads_code_input(message: Message, state: FSMContext, session): try: await create_tracking_source( - name=name, - code=code_with_prefix, - type_="utm", - created_by=message.from_user.id, - session=session + name=name, code=code_with_prefix, type_="utm", created_by=message.from_user.id, session=session ) stats = await get_tracking_source_stats(code_with_prefix, session) if not stats: @@ -81,7 +72,7 @@ async def handle_ads_code_input(message: Message, state: FSMContext, session): return msg = format_ads_stats(stats, USERNAME_BOT) await message.answer( - text=msg, + text=msg, reply_markup=build_ads_stats_kb(code_with_prefix), ) @@ -92,13 +83,14 @@ async def handle_ads_code_input(message: Message, state: FSMContext, session): await state.clear() - @router.callback_query(AdminAdsCallback.filter(F.action == "list"), IsAdminFilter()) async def handle_ads_list(callback_query: CallbackQuery, session): try: ads = await get_all_tracking_sources(session) reply_markup = build_ads_list_kb(ads, current_page=1, total_pages=1) - await callback_query.message.edit_text("📋 Выберите ссылку для просмотра статистики:", reply_markup=reply_markup) + await callback_query.message.edit_text( + "📋 Выберите ссылку для просмотра статистики:", reply_markup=reply_markup + ) except Exception as e: logger.error(f"Ошибка при получении списка UTM: {e}") await callback_query.message.edit_text("Произошла ошибка при получении списка.") @@ -113,7 +105,9 @@ async def handle_ads_view(callback_query: CallbackQuery, callback_data: AdminAds await callback_query.message.edit_text("❌ Источник не найден или не содержит данных.") return msg = format_ads_stats(stats, USERNAME_BOT) - await callback_query.message.edit_text(text=msg, reply_markup=build_ads_stats_kb(code), parse_mode=ParseMode.HTML) + await callback_query.message.edit_text( + text=msg, reply_markup=build_ads_stats_kb(code), parse_mode=ParseMode.HTML + ) except Exception as e: logger.error(f"Ошибка при просмотре статистики: {e}") await callback_query.message.edit_text("❌ Ошибка при получении статистики.") @@ -132,15 +126,9 @@ async def handle_ads_delete_confirm(callback_query: CallbackQuery, callback_data async def handle_ads_delete(callback_query: CallbackQuery, callback_data: AdminAdsCallback, session): code = callback_data.code try: - await session.execute( - "UPDATE users SET source_code = NULL WHERE source_code = $1", - code - ) + await session.execute("UPDATE users SET source_code = NULL WHERE source_code = $1", code) await session.execute("DELETE FROM tracking_sources WHERE code = $1", code) - await callback_query.message.edit_text( - f"🗑️ Ссылка {code} удалена.", - reply_markup=build_ads_kb() - ) + await callback_query.message.edit_text(f"🗑️ Ссылка {code} удалена.", reply_markup=build_ads_kb()) except Exception as e: logger.error(f"Ошибка при удалении метки {code}: {e}", exc_info=True) await callback_query.message.edit_text("❌ Не удалось удалить ссылку.") @@ -152,14 +140,11 @@ def format_ads_stats(stats: dict, username_bot: str) -> str: f"📌 Название: {stats['name']}\n" f"🔗 Ссылка: https://t.me/{username_bot}?start={stats['code']}\n" f"🕓 Создана: {stats['created_at'].strftime('%d.%m.%Y %H:%M')}\n\n" - f"💡 Активность:\n" f"└ 🆕 Регистраций: {stats.get('registrations', 0)}\n" f"└ 🧪 Триалов: {stats.get('trials', 0)}\n" - f"\n💰 Финансовая информация:\n" f"└ 💳 Покупок: {stats.get('payments', 0)}\n\n" - f"Просмотр статистики и управление рекламными ссылками." ) @@ -167,7 +152,4 @@ def format_ads_stats(stats: dict, username_bot: str) -> str: @router.callback_query(AdminAdsCallback.filter(F.action == "cancel_input"), IsAdminFilter()) async def handle_ads_cancel_input(callback_query: CallbackQuery, state: FSMContext): await state.clear() - await callback_query.message.edit_text( - text="📊 Аналитика рекламы:", - reply_markup=build_ads_kb() - ) + await callback_query.message.edit_text(text="📊 Аналитика рекламы:", reply_markup=build_ads_kb()) diff --git a/handlers/admin/ads/keyboard.py b/handlers/admin/ads/keyboard.py index fb46a531..5faf0e32 100644 --- a/handlers/admin/ads/keyboard.py +++ b/handlers/admin/ads/keyboard.py @@ -1,6 +1,6 @@ -from aiogram.utils.keyboard import InlineKeyboardBuilder -from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton from aiogram.filters.callback_data import CallbackData +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from aiogram.utils.keyboard import InlineKeyboardBuilder from ..panel.keyboard import build_admin_back_btn @@ -9,6 +9,7 @@ class AdminAdsCallback(CallbackData, prefix="admin_ads"): action: str code: str | None = None + def build_ads_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button(text="➕ Новая ссылка", callback_data=AdminAdsCallback(action="create").pack()) @@ -60,20 +61,17 @@ def build_ads_delete_confirm_kb(code: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( text="✅ Да, удалить", - callback_data=AdminAdsCallback(action="delete", code=code,).pack() - ) - builder.button( - text="❌ Отмена", - callback_data=AdminAdsCallback(action="view", code=code).pack() + callback_data=AdminAdsCallback( + action="delete", + code=code, + ).pack(), ) + builder.button(text="❌ Отмена", callback_data=AdminAdsCallback(action="view", code=code).pack()) builder.adjust(1) return builder.as_markup() def build_cancel_input_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button( - text="❌ Отмена", - callback_data=AdminAdsCallback(action="cancel_input", code="none").pack() - ) + builder.button(text="❌ Отмена", callback_data=AdminAdsCallback(action="cancel_input", code="none").pack()) return builder.as_markup() diff --git a/handlers/admin/clusters/clusters_handler.py b/handlers/admin/clusters/clusters_handler.py index e16e224e..33b59d35 100644 --- a/handlers/admin/clusters/clusters_handler.py +++ b/handlers/admin/clusters/clusters_handler.py @@ -1,7 +1,6 @@ import asyncio import time -from datetime import datetime, timedelta from typing import Any import asyncpg diff --git a/handlers/admin/clusters/keyboard.py b/handlers/admin/clusters/keyboard.py index a36a0b66..9e087c04 100644 --- a/handlers/admin/clusters/keyboard.py +++ b/handlers/admin/clusters/keyboard.py @@ -4,7 +4,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from handlers.buttons import BACK -from ..panel.keyboard import build_admin_back_btn, AdminPanelCallback +from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn from ..servers.keyboard import AdminServerCallback @@ -96,12 +96,7 @@ def build_cluster_management_kb(cluster_name: str) -> InlineKeyboardMarkup: callback_data=AdminClusterCallback(action="rename", data=cluster_name).pack(), ) ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", - callback_data=AdminPanelCallback(action="clusters").pack() - ) - ) + builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=AdminPanelCallback(action="clusters").pack())) return builder.as_markup() diff --git a/handlers/admin/panel/keyboard.py b/handlers/admin/panel/keyboard.py index 709620be..a6813f35 100644 --- a/handlers/admin/panel/keyboard.py +++ b/handlers/admin/panel/keyboard.py @@ -26,7 +26,6 @@ def build_panel_kb() -> InlineKeyboardMarkup: InlineKeyboardButton(text="🤖 Управление", callback_data=AdminPanelCallback(action="management").pack()), ) builder.row( - InlineKeyboardButton(text="📢 Рассылка", callback_data=AdminPanelCallback(action="sender").pack()), InlineKeyboardButton(text="🎟️ Купоны", callback_data=AdminPanelCallback(action="coupons").pack()), ) diff --git a/handlers/admin/sender/keyboard.py b/handlers/admin/sender/keyboard.py index 6e3c25ce..46438dcb 100644 --- a/handlers/admin/sender/keyboard.py +++ b/handlers/admin/sender/keyboard.py @@ -23,11 +23,7 @@ def build_sender_kb() -> InlineKeyboardMarkup: text="📍 Не использовавшие триал", callback_data=AdminSenderCallback(type="untrial").pack() ) ) - builder.row( - InlineKeyboardButton( - text="🔥 Горячие лиды", callback_data=AdminSenderCallback(type="hotleads").pack() - ) - ) + builder.row(InlineKeyboardButton(text="🔥 Горячие лиды", callback_data=AdminSenderCallback(type="hotleads").pack())) builder.row( InlineKeyboardButton(text="📢 Кластер", callback_data=AdminSenderCallback(type="cluster-select").pack()) ) diff --git a/handlers/admin/servers/keyboard.py b/handlers/admin/servers/keyboard.py index 826f2c9b..6609c278 100644 --- a/handlers/admin/servers/keyboard.py +++ b/handlers/admin/servers/keyboard.py @@ -18,30 +18,19 @@ def build_manage_server_kb(server_name: str, cluster_name: str, enabled: bool) - toggle_text = "🔴 Отключить" if enabled else "🟢 Включить" toggle_action = "disable" if enabled else "enable" - builder.button( - text=toggle_text, - callback_data=AdminServerCallback(action=toggle_action, data=server_name).pack() - ) + builder.button(text=toggle_text, callback_data=AdminServerCallback(action=toggle_action, data=server_name).pack()) builder.button( - text="📈 Задать лимит", - callback_data=AdminServerCallback(action="set_limit", data=server_name).pack() + text="📈 Задать лимит", callback_data=AdminServerCallback(action="set_limit", data=server_name).pack() ) - builder.button( - text="🗑️ Удалить", - callback_data=AdminServerCallback(action="delete", data=server_name).pack() - ) + builder.button(text="🗑️ Удалить", callback_data=AdminServerCallback(action="delete", data=server_name).pack()) builder.button( - text="✏️ Сменить название", - callback_data=AdminServerCallback(action="rename", data=server_name).pack() + text="✏️ Сменить название", callback_data=AdminServerCallback(action="rename", data=server_name).pack() ) - builder.button( - text=BACK, - callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack() - ) + builder.button(text=BACK, callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack()) builder.adjust(1) - return builder.as_markup() \ No newline at end of file + return builder.as_markup() diff --git a/handlers/admin/servers/servers_handler.py b/handlers/admin/servers/servers_handler.py index ec161bb3..69bdfd08 100644 --- a/handlers/admin/servers/servers_handler.py +++ b/handlers/admin/servers/servers_handler.py @@ -9,12 +9,14 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from database import get_servers from filters.admin import IsAdminFilter from handlers.buttons import BACK + from ..panel.keyboard import build_admin_back_kb from .keyboard import ( AdminServerCallback, build_manage_server_kb, ) + router = Router() @@ -178,20 +180,14 @@ async def process_callback_delete_server( ) -@router.callback_query( - AdminServerCallback.filter(F.action.in_(["enable", "disable"])), IsAdminFilter() -) +@router.callback_query(AdminServerCallback.filter(F.action.in_(["enable", "disable"])), IsAdminFilter()) async def toggle_server_enabled(callback_query: CallbackQuery, callback_data: AdminServerCallback, session: Any): server_name = callback_data.data action = callback_data.action new_status = action == "enable" - await session.execute( - "UPDATE servers SET enabled = $1 WHERE server_name = $2", - new_status, - server_name - ) + await session.execute("UPDATE servers SET enabled = $1 WHERE server_name = $2", new_status, server_name) servers = await get_servers(include_enabled=True) @@ -241,10 +237,7 @@ async def save_server_limit(message: types.Message, state: FSMContext, session: server_name = data["server_name"] new_value = limit if limit > 0 else None - await session.execute( - "UPDATE servers SET max_keys = $1 WHERE server_name = $2", - new_value, server_name - ) + await session.execute("UPDATE servers SET max_keys = $1 WHERE server_name = $2", new_value, server_name) servers = await get_servers(include_enabled=True) cluster_name, server = next( @@ -268,8 +261,7 @@ async def save_server_limit(message: types.Message, state: FSMContext, session: ) await message.answer( - text, - reply_markup=build_manage_server_kb(server_name, cluster_name, enabled=server.get("enabled", True)) + text, reply_markup=build_manage_server_kb(server_name, cluster_name, enabled=server.get("enabled", True)) ) await state.clear() diff --git a/handlers/admin/users/users_handler.py b/handlers/admin/users/users_handler.py index 8a896127..83fabe76 100644 --- a/handlers/admin/users/users_handler.py +++ b/handlers/admin/users/users_handler.py @@ -1,6 +1,6 @@ import asyncio -import uuid import time +import uuid from datetime import datetime, timedelta, timezone from typing import Any @@ -20,12 +20,12 @@ from database import ( delete_user_data, get_balance, get_client_id_by_email, + get_key_details, get_servers, + set_user_balance, update_balance, update_key_expiry, update_trial, - get_key_details, - set_user_balance ) from filters.admin import IsAdminFilter from handlers.keys.key_utils import ( @@ -628,12 +628,8 @@ async def process_user_search( created_at = user_data["created_at"].astimezone(MOSCOW_TZ).strftime("%H:%M:%S %d.%m.%Y") updated_at = user_data["updated_at"].astimezone(MOSCOW_TZ).strftime("%H:%M:%S %d.%m.%Y") - referral_count = await session.fetchval( - "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id - ) - key_records = await session.fetch( - "SELECT email, expiry_time FROM keys WHERE tg_id = $1", tg_id - ) + referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) + key_records = await session.fetch("SELECT email, expiry_time FROM keys WHERE tg_id = $1", tg_id) text = ( f"📊 Информация о пользователе" diff --git a/handlers/captcha.py b/handlers/captcha.py index 5df53f65..f1ed579b 100644 --- a/handlers/captcha.py +++ b/handlers/captcha.py @@ -8,7 +8,7 @@ from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, Message from aiogram.utils.keyboard import InlineKeyboardBuilder -from handlers.texts import CAPTCHA_PROMPT_MSG, CAPTCHA_EMOJIS +from handlers.texts import CAPTCHA_EMOJIS, CAPTCHA_PROMPT_MSG from logger import logger from .utils import edit_or_send_message diff --git a/handlers/coupons.py b/handlers/coupons.py index fbff35d4..5cc576a4 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -14,8 +14,8 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from config import ADMIN_ID from database import ( add_user, - check_user_exists, check_coupon_usage, + check_user_exists, create_coupon_usage, get_coupon_by_code, get_keys, diff --git a/handlers/keys/key_freeze.py b/handlers/keys/key_freeze.py index c655dc4d..203875f6 100644 --- a/handlers/keys/key_freeze.py +++ b/handlers/keys/key_freeze.py @@ -98,7 +98,6 @@ async def process_callback_unfreeze_subscription_confirm(callback_query: Callbac added_days = max(leftover / (1000 * 86400), 0.01) total_gb = int((added_days / 30) * TOTAL_GB * 1024**3) - await renew_key_in_cluster( cluster_id=cluster_id, email=email, diff --git a/handlers/keys/key_mode/key_country_mode.py b/handlers/keys/key_mode/key_country_mode.py index 95f1ac6e..64a93b2a 100644 --- a/handlers/keys/key_mode/key_country_mode.py +++ b/handlers/keys/key_mode/key_country_mode.py @@ -24,8 +24,8 @@ from config import ( ) from database import ( add_user, - check_user_exists, check_server_name_by_cluster, + check_user_exists, get_key_details, get_trial, update_balance, @@ -247,7 +247,6 @@ async def finalize_key_creation( ) logger.info(f"[User] Новый пользователь {tg_id} добавлен") - expiry_time = expiry_time.astimezone(moscow_tz) if old_key_name: @@ -459,7 +458,7 @@ async def check_server_availability(server_info: dict, session: Any) -> bool: panel_type = server_info.get("panel_type", "3x-ui").lower() enabled = server_info.get("enabled", True) max_keys = server_info.get("max_keys") - + if not enabled: logger.info(f"[Ping] Сервер {server_name} выключен (enabled = FALSE).") return False @@ -494,4 +493,4 @@ async def check_server_availability(server_info: dict, session: Any) -> bool: return False except Exception as e: logger.warning(f"[Ping] Ошибка при проверке сервера {server_name}: {e}") - return False \ No newline at end of file + return False diff --git a/handlers/keys/key_mode/key_create.py b/handlers/keys/key_mode/key_create.py index 90c9efe9..e84ea9c9 100644 --- a/handlers/keys/key_mode/key_create.py +++ b/handlers/keys/key_mode/key_create.py @@ -180,11 +180,7 @@ async def create_key( Также отвечает за первичное подключение пользователя. """ if not await check_user_exists(tg_id): - from_user = ( - message_or_query.from_user - if isinstance(message_or_query, (CallbackQuery, Message)) - else None - ) + from_user = message_or_query.from_user if isinstance(message_or_query, CallbackQuery | Message) else None if from_user: await add_user( tg_id=from_user.id, diff --git a/handlers/keys/key_renew.py b/handlers/keys/key_renew.py index 7e0663a1..6d7f8d4b 100644 --- a/handlers/keys/key_renew.py +++ b/handlers/keys/key_renew.py @@ -76,7 +76,7 @@ async def process_callback_renew_key(callback_query: CallbackQuery, session: Any ) ) - builder.row(InlineKeyboardButton(text=BACK, callback_data="view_keys")) + builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{record['email']}")) balance = await get_balance(tg_id) diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index 86c5c458..4fb06dcd 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -5,6 +5,7 @@ from typing import Any import asyncpg +from bot import bot from config import ( DATABASE_URL, LIMIT_IP, @@ -15,7 +16,7 @@ from config import ( TOTAL_GB, ) from database import delete_notification, get_servers, store_key -from handlers.utils import get_least_loaded_cluster, check_server_key_limit +from handlers.utils import check_server_key_limit, get_least_loaded_cluster from logger import logger from panels.remnawave import RemnawaveAPI from panels.three_xui import ( @@ -24,12 +25,10 @@ from panels.three_xui import ( delete_client, extend_client_key, get_client_traffic, + get_xui_instance, toggle_client, - get_xui_instance ) -from bot import bot - async def create_key_on_cluster( cluster_id: str, @@ -66,14 +65,14 @@ async def create_key_on_cluster( async with asyncpg.create_pool(DATABASE_URL) as pool: async with pool.acquire() as conn: remnawave_servers = [ - s for s in enabled_servers - if s.get("panel_type", "3x-ui").lower() == "remnawave" - and await check_server_key_limit(s, conn) + s + for s in enabled_servers + if s.get("panel_type", "3x-ui").lower() == "remnawave" and await check_server_key_limit(s, conn) ] xui_servers = [ - s for s in enabled_servers - if s.get("panel_type", "3x-ui").lower() == "3x-ui" - and await check_server_key_limit(s, conn) + s + for s in enabled_servers + if s.get("panel_type", "3x-ui").lower() == "3x-ui" and await check_server_key_limit(s, conn) ] if not remnawave_servers and not xui_servers: @@ -789,4 +788,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 a809c2ab..33751f28 100644 --- a/handlers/keys/key_view.py +++ b/handlers/keys/key_view.py @@ -17,7 +17,6 @@ from config import ( CONNECT_PHONE_BUTTON, ENABLE_DELETE_KEY_BUTTON, ENABLE_UPDATE_SUBSCRIPTION_BUTTON, - PUBLIC_LINK, QRCODE, TOGGLE_CLIENT, USE_COUNTRY_SELECTION, @@ -48,7 +47,14 @@ from handlers.texts import ( NO_SUBSCRIPTIONS_MSG, key_message, ) -from handlers.utils import edit_or_send_message, handle_error, is_full_remnawave_cluster, format_days, format_hours, format_minutes +from handlers.utils import ( + edit_or_send_message, + format_days, + format_hours, + format_minutes, + handle_error, + is_full_remnawave_cluster, +) from logger import logger @@ -221,7 +227,9 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any) days = total_seconds // 86400 hours = (total_seconds % 86400) // 3600 minutes = (total_seconds % 3600) // 60 - days_left_message = f"Осталось: {format_days(days)}, {format_hours(hours)}, {format_minutes(minutes)}" + days_left_message = ( + f"Осталось: {format_days(days)}, {format_hours(hours)}, {format_minutes(minutes)}" + ) formatted_expiry_date = expiry_date.strftime("%d %B %Y года") response_message = key_message( diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index 02dfba77..820a9023 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -135,7 +135,9 @@ def format_time_left(expiry_time_ms: int | None) -> str: return f"{days}D,{hours}H ⏳" if days else f"{hours}H ⏳" -def prepare_headers(user_agent: str, project_name: str, subscription_info: str, subscription_userinfo: str) -> dict[str, str]: +def prepare_headers( + user_agent: str, project_name: str, subscription_info: str, subscription_userinfo: str +) -> dict[str, str]: if "Happ" in user_agent: encoded_project_name = f"{project_name}" announce_str = f"↖️Бот | {subscription_info} | Поддержка↗️" @@ -160,7 +162,7 @@ def prepare_headers(user_agent: str, project_name: str, subscription_info: str, } elif "v2raytun" in user_agent: encoded_project_name = f"{project_name}\n{subscription_info}" - announce_str = f"🔑 Выберите сервер ⬇️ | 💬 Поддержка ➡️" + announce_str = "🔑 Выберите сервер ⬇️ | 💬 Поддержка ➡️" return { "Content-Type": "text/plain; charset=utf-8", "Content-Disposition": "inline", diff --git a/handlers/notifications/general_notifications.py b/handlers/notifications/general_notifications.py index ba9fbdf9..87d72713 100644 --- a/handlers/notifications/general_notifications.py +++ b/handlers/notifications/general_notifications.py @@ -22,8 +22,8 @@ from config import ( ) from database import ( add_notification, - check_notifications_bulk, check_notification_time, + check_notifications_bulk, delete_key, delete_notification, get_all_keys, @@ -48,7 +48,7 @@ from handlers.texts import ( from handlers.utils import format_hours, format_minutes from logger import logger -from .notify_utils import send_notification, send_messages_with_limit +from .notify_utils import send_messages_with_limit, send_notification from .special_notifications import notify_inactive_trial_users, notify_users_no_traffic @@ -331,19 +331,15 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: if hours > 0: if minutes > 0: delay_message = KEY_EXPIRED_DELAY_HOURS_MINUTES_MSG.format( - email=email, - hours_formatted=format_hours(hours), - minutes_formatted=format_minutes(minutes) + email=email, hours_formatted=format_hours(hours), minutes_formatted=format_minutes(minutes) ) else: delay_message = KEY_EXPIRED_DELAY_HOURS_MSG.format( - email=email, - hours_formatted=format_hours(hours) + email=email, hours_formatted=format_hours(hours) ) else: delay_message = KEY_EXPIRED_DELAY_MINUTES_MSG.format( - email=email, - minutes_formatted=format_minutes(minutes) + email=email, minutes_formatted=format_minutes(minutes) ) else: delay_message = KEY_EXPIRED_NO_DELAY_MSG.format(email=email) @@ -431,7 +427,9 @@ async def process_auto_renew_or_notify( if result: logger.info(f"✅ Уведомление о продлении подписки {email} отправлено пользователю {tg_id}.") else: - logger.warning(f"📢 Не удалось отправить уведомление о продлении подписки {email} пользователю {tg_id}.") + logger.warning( + f"📢 Не удалось отправить уведомление о продлении подписки {email} пользователю {tg_id}." + ) except KeyError as e: logger.error(f"❌ Ошибка форматирования сообщения KEY_RENEWED: отсутствует ключ {e}") except Exception as e: @@ -443,4 +441,4 @@ async def process_auto_renew_or_notify( logger.info(f"📢 Отправлено уведомление об истекающей подписке {email} пользователю {tg_id}.") await add_notification(tg_id, notification_id, session=conn) else: - logger.warning(f"📢 Не удалось отправить уведомление об истекающей подписке {email} пользователю {tg_id}.") \ No newline at end of file + logger.warning(f"📢 Не удалось отправить уведомление об истекающей подписке {email} пользователю {tg_id}.") diff --git a/handlers/notifications/notify_utils.py b/handlers/notifications/notify_utils.py index a6de8b37..679db645 100644 --- a/handlers/notifications/notify_utils.py +++ b/handlers/notifications/notify_utils.py @@ -16,16 +16,10 @@ async def send_messages_with_limit(bot: Bot, messages: list[dict], messages_per_ """ batch_size = messages_per_second for i in range(0, len(messages), batch_size): - batch = messages[i:i + batch_size] + batch = messages[i : i + batch_size] tasks = [] for msg in batch: - tasks.append(send_notification( - bot, - msg["tg_id"], - msg.get("photo"), - msg["text"], - msg.get("keyboard") - )) + tasks.append(send_notification(bot, msg["tg_id"], msg.get("photo"), msg["text"], msg.get("keyboard"))) try: await asyncio.gather(*tasks, return_exceptions=True) except Exception as e: @@ -50,6 +44,7 @@ def rate_limited_send(func): tg_id = kwargs.get("tg_id") or args[1] logger.error(f"❌ Ошибка отправки сообщения пользователю {tg_id}: {e}") return False + return wrapper @@ -65,7 +60,7 @@ async def send_notification( """ if image_filename is None: return await _send_text_notification(bot, tg_id, caption, keyboard) - + photo_path = os.path.join("img", image_filename) if os.path.isfile(photo_path): return await _send_photo_notification(bot, tg_id, photo_path, image_filename, caption, keyboard) @@ -114,4 +109,4 @@ async def _send_text_notification( return False except Exception as e: logger.error(f"Неизвестная ошибка при отправке сообщения для пользователя {tg_id}: {e}") - return False \ No newline at end of file + return False diff --git a/handlers/notifications/special_notifications.py b/handlers/notifications/special_notifications.py index 2110a2b4..bbfb9a8f 100644 --- a/handlers/notifications/special_notifications.py +++ b/handlers/notifications/special_notifications.py @@ -20,7 +20,8 @@ from handlers.keys.key_utils import get_user_traffic from handlers.texts import TRIAL_INACTIVE_BONUS_MSG, TRIAL_INACTIVE_FIRST_MSG, ZERO_TRAFFIC_MSG from handlers.utils import format_days from logger import logger -from .notify_utils import send_notification, send_messages_with_limit + +from .notify_utils import send_messages_with_limit, send_notification router = Router() @@ -70,8 +71,7 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): await conn.execute("UPDATE users SET trial = -1 WHERE tg_id = $1", tg_id) else: message = TRIAL_INACTIVE_FIRST_MSG.format( - display_name=display_name, - trial_time_formatted=format_days(TRIAL_TIME) + display_name=display_name, trial_time_formatted=format_days(TRIAL_TIME) ) messages.append({ @@ -175,4 +175,4 @@ async def notify_users_no_traffic(bot: Bot, conn: asyncpg.Connection, current_ti except Exception as e: logger.error(f"Ошибка обновления notified для пользователя {tg_id}: {e}") - logger.info("✅ Обработка пользователей с нулевым трафиком завершена.") \ No newline at end of file + logger.info("✅ Обработка пользователей с нулевым трафиком завершена.") diff --git a/handlers/pay.py b/handlers/pay.py index 6ed10c0f..d363795f 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -1,8 +1,10 @@ +import os + +from typing import Any + from aiogram import F, Router from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from typing import Any -import os from config import ( CRYPTO_BOT_ENABLE, @@ -12,9 +14,19 @@ from config import ( YOOKASSA_ENABLE, YOOMONEY_ENABLE, ) -from handlers.buttons import CRYPTOBOT, MAIN_MENU, ROBOKASSA, STARS, YOOKASSA, YOOMONEY, PAYMENT, BALANCE_HISTORY, COUPON -from handlers.texts import PAYMENT_METHODS_MSG, BALANCE_HISTORY_HEADER, BALANCE_MANAGEMENT_TEXT from database import get_last_payments +from handlers.buttons import ( + BALANCE_HISTORY, + COUPON, + CRYPTOBOT, + MAIN_MENU, + PAYMENT, + ROBOKASSA, + STARS, + YOOKASSA, + YOOMONEY, +) +from handlers.texts import BALANCE_HISTORY_HEADER, BALANCE_MANAGEMENT_TEXT, PAYMENT_METHODS_MSG from .utils import edit_or_send_message @@ -131,4 +143,4 @@ async def balance_history_handler(callback_query: CallbackQuery, session: Any): reply_markup=builder.as_markup(), media_path=None, disable_web_page_preview=False, - ) \ No newline at end of file + ) diff --git a/handlers/refferal.py b/handlers/refferal.py index 3927e72c..658f087c 100644 --- a/handlers/refferal.py +++ b/handlers/refferal.py @@ -1,15 +1,14 @@ import os from io import BytesIO - from typing import Any import asyncpg import qrcode from aiogram import F, Router -from aiogram.fsm.context import FSMContext from aiogram.enums import ParseMode +from aiogram.fsm.context import FSMContext from aiogram.types import ( CallbackQuery, InlineKeyboardButton, @@ -20,6 +19,7 @@ from aiogram.types import ( ) from aiogram.utils.keyboard import InlineKeyboardBuilder +from bot import bot from config import ( ADMIN_ID, DATABASE_URL, @@ -28,7 +28,7 @@ from config import ( TRIAL_TIME, USERNAME_BOT, ) -from database import get_referral_stats, get_referral_by_referred_id, add_referral, add_user, check_user_exists +from database import add_referral, add_user, check_user_exists, get_referral_by_referred_id, get_referral_stats from handlers.buttons import ( BACK, INVITE, @@ -36,17 +36,22 @@ from handlers.buttons import ( QR, TOP_FIVE, ) -from handlers.texts import INVITE_TEXT_NON_INLINE, TOP_REFERRALS_TEXT, REFERRAL_OFFERS, REFERRAL_SUCCESS_MSG, NEW_REFERRAL_NOTIFICATION +from handlers.texts import ( + INVITE_TEXT_NON_INLINE, + NEW_REFERRAL_NOTIFICATION, + REFERRAL_OFFERS, + REFERRAL_SUCCESS_MSG, + TOP_REFERRALS_TEXT, +) from logger import logger from .texts import get_referral_link, invite_message_send from .utils import edit_or_send_message, format_days -from bot import bot - router = Router() + @router.callback_query(F.data == "invite") @router.message(F.text == "/invite") async def invite_handler(callback_query_or_message: Message | CallbackQuery): @@ -253,4 +258,4 @@ async def handle_referral_link(referral_code: str, message: Message, state: FSMC except Exception as e: logger.error(f"Ошибка при обработке реферальной ссылки {referral_code}: {e}") await message.answer("❌ Произошла ошибка при обработке реферальной ссылки.") - return \ No newline at end of file + return diff --git a/handlers/start.py b/handlers/start.py index 73637ba7..1927cf35 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -24,14 +24,14 @@ from config import ( SUPPORT_CHAT_URL, ) from database import ( - get_trial, add_user, check_user_exists, + get_trial, ) -from handlers.payments.gift import handle_gift_link 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.payments.gift import handle_gift_link from handlers.profile import process_callback_view_profile from handlers.texts import ( NOT_SUBSCRIBED_YET_MSG, @@ -44,8 +44,8 @@ from handlers.texts import ( from logger import logger from .admin.panel.keyboard import AdminPanelCallback -from .utils import edit_or_send_message from .refferal import handle_referral_link +from .utils import edit_or_send_message router = Router() @@ -219,14 +219,9 @@ async def process_start_logic( async def handle_utm_link(utm_code, message, state, session): - - exists = await session.fetchval( - "SELECT 1 FROM tracking_sources WHERE code = $1", utm_code - ) + exists = await session.fetchval("SELECT 1 FROM tracking_sources WHERE code = $1", utm_code) if exists: - current_code = await session.fetchval( - "SELECT source_code FROM users WHERE tg_id = $1", message.chat.id - ) + current_code = await session.fetchval("SELECT source_code FROM users WHERE tg_id = $1", message.chat.id) if current_code is None: user_exists = await check_user_exists(message.chat.id) from_user = message.from_user @@ -242,10 +237,7 @@ async def handle_utm_link(utm_code, message, state, session): source_code=utm_code, ) else: - await session.execute( - "UPDATE users SET source_code = $1 WHERE tg_id = $2", - utm_code, message.chat.id - ) + await session.execute("UPDATE users SET source_code = $1 WHERE tg_id = $2", utm_code, message.chat.id) logger.info(f"[UTM] Привязана {utm_code} к пользователю {message.chat.id}") else: await message.answer("❌ UTM ссылка не найдена.") diff --git a/handlers/utils.py b/handlers/utils.py index dc4b09d6..189bae49 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -11,7 +11,7 @@ import asyncpg from aiogram.types import BufferedInputFile, InlineKeyboardMarkup, InputMediaPhoto, Message from bot import bot -from config import DATABASE_URL, ADMIN_ID +from config import ADMIN_ID, DATABASE_URL from database import get_all_keys, get_servers from logger import logger @@ -85,8 +85,7 @@ async def check_server_key_limit(server_info: dict, conn) -> bool: if usage_percent >= 0.9: notif_key = f"server_warn_{server_name}" already_sent = await conn.fetchval( - "SELECT EXISTS (SELECT 1 FROM notifications WHERE tg_id = 0 AND notification_type = $1)", - notif_key + "SELECT EXISTS (SELECT 1 FROM notifications WHERE tg_id = 0 AND notification_type = $1)", notif_key ) if not already_sent: for admin_id in ADMIN_ID: @@ -130,12 +129,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_months(months: int) -> str: """Форматирует количество месяцев с правильным склонением""" if months <= 0: return "0 месяцев" return f"{months} {get_plural_form(months, 'месяц', 'месяца', 'месяцев')}" + def format_days(days: int) -> str: """ Форматирует количество дней с правильным склонением. @@ -144,12 +145,14 @@ def format_days(days: int) -> str: return "0 дней" return f"{days} {get_plural_form(days, 'день', 'дня', 'дней')}" + def format_hours(hours: int) -> str: """Форматирует количество часов с правильным склонением""" if hours <= 0: return "0 часов" return f"{hours} {get_plural_form(hours, 'час', 'часа', 'часов')}" + def format_minutes(minutes: int) -> str: """Форматирует количество минут с правильным склонением""" if minutes <= 0: @@ -252,4 +255,4 @@ def sanitize_key_name(key_name: str) -> str: Returns: str: Очищенное название ключа в нижнем регистре. """ - return re.sub(r"[^a-z0-9@._-]", "", key_name.lower()) \ No newline at end of file + return re.sub(r"[^a-z0-9@._-]", "", key_name.lower()) diff --git a/panels/three_xui.py b/panels/three_xui.py index 0ea7c5ff..b932ef86 100644 --- a/panels/three_xui.py +++ b/panels/three_xui.py @@ -1,18 +1,20 @@ +import time + from dataclasses import dataclass from typing import Any import httpx import py3xui + from py3xui import AsyncApi -import time from config import ( - LIMIT_IP, - SUPERNODE, - XUI_TOKEN, - USE_XUI_TOKEN, ADMIN_PASSWORD, ADMIN_USERNAME, + LIMIT_IP, + SUPERNODE, + USE_XUI_TOKEN, + XUI_TOKEN, ) from logger import logger @@ -36,6 +38,7 @@ class ClientConfig: _xui_instance_cache: dict[str, tuple[AsyncApi, float]] = {} SESSION_TTL = 1800 + async def get_xui_instance(api_url: str) -> AsyncApi: key = f"{api_url}|{ADMIN_USERNAME}" current_time = time.time() @@ -46,7 +49,7 @@ async def get_xui_instance(api_url: str) -> AsyncApi: if current_time - last_login < SESSION_TTL: return xui else: - logger.info(f"[XUI Cache] Сессия устарела (>30 минут), переподключение...") + logger.info("[XUI Cache] Сессия устарела (>30 минут), переподключение...") await xui.login() _xui_instance_cache[key] = (xui, current_time) return xui @@ -65,7 +68,6 @@ async def get_xui_instance(api_url: str) -> AsyncApi: async def add_client(xui: py3xui.AsyncApi, config: ClientConfig) -> dict[str, Any]: try: - client = py3xui.Client( id=config.client_id, email=config.email.lower(), @@ -107,7 +109,6 @@ async def extend_client_key( tg_id: int, ) -> bool | None: try: - client = await xui.client.get_by_email(email) if not client or not client.id: logger.warning(f"Клиент с email {email} не найден или не имеет ID.") @@ -158,7 +159,6 @@ async def delete_client( bool: True если удаление успешно, False в противном случае """ try: - if SUPERNODE: await xui.client.delete(inbound_id, client_id) logger.info(f"Клиент с ID {client_id} был удален успешно (SUPERNODE)") @@ -185,7 +185,6 @@ async def delete_client( async def get_client_traffic(xui: py3xui.AsyncApi, client_id: str) -> dict[str, Any]: try: - traffic_data = await xui.client.get_traffic_by_id(client_id) if not traffic_data: logger.warning(f"Трафик для клиента {client_id} не найден.") @@ -205,7 +204,6 @@ async def get_client_traffic(xui: py3xui.AsyncApi, client_id: str) -> dict[str, async def toggle_client(xui: py3xui.AsyncApi, inbound_id: int, email: str, client_id: str, enable: bool = True) -> bool: try: - client = await xui.client.get_by_email(email) if not client: logger.warning(f"Клиент с email {email} и ID {client_id} не найден.")