From 4b2e28d15dd90bdcab29807431f0512c51a5dba8 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Wed, 15 Jan 2025 02:15:21 +0300 Subject: [PATCH 01/49] Delete parse_mode --- handlers/admin/admin_servers.py | 13 +------------ handlers/coupons.py | 2 +- handlers/instructions/instructions.py | 4 +--- handlers/utils.py | 2 +- servers.py | 1 - 5 files changed, 4 insertions(+), 18 deletions(-) diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index 17be59c8..53792c59 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -50,7 +50,6 @@ async def handle_servers_editor(callback_query: types.CallbackQuery): "🌐 Кластеры — это пространство серверов, в пределах которого создается подписка.\n" "💡 Если вы хотите выдавать по 1 серверу, то добавьте всего 1 сервер в кластер.\n\n" "⚠️ Важно: Кластеры удаляются автоматически, если удалить все серверы внутри них.\n\n", - parse_mode="HTML", reply_markup=builder.as_markup(), ) @@ -60,8 +59,7 @@ async def handle_add_cluster(callback_query: types.CallbackQuery, state: FSMCont await callback_query.message.answer( "🔧 Введите имя нового кластера:\n\n" "Имя кластера должно быть уникальным и на английском языке.\n" - "Пример: cluster2 или us_east_1", - parse_mode="HTML", + "Пример: cluster2 или us_east_1" ) await state.set_state(UserEditorState.waiting_for_cluster_name) @@ -100,7 +98,6 @@ async def handle_cluster_name_input(message: types.Message, state: FSMContext): f"Введите имя сервера для кластера {cluster_name}:\n\n" "Рекомендуется указать локацию сервера в имени.\n\n" "Пример: server-asia, server-europe", - parse_mode="HTML", reply_markup=builder.as_markup(), ) await state.set_state(UserEditorState.waiting_for_server_name) @@ -149,7 +146,6 @@ async def handle_server_name_input(message: types.Message, state: FSMContext): "API URL должен быть в следующем формате:\n\n" "https://your_domain:port/panel_path\n\n" "URL должен быть без слэша на конце!\n", - parse_mode="HTML", reply_markup=builder.as_markup(), ) await state.set_state(UserEditorState.waiting_for_api_url) @@ -176,7 +172,6 @@ async def handle_api_url_input(message: types.Message, state: FSMContext): if not api_url.startswith("https://"): await message.answer( "❌ API URL должен начинаться с https://. Попробуйте снова.", - parse_mode="HTML", ) return @@ -198,7 +193,6 @@ async def handle_api_url_input(message: types.Message, state: FSMContext): "https://your_domain:port_sub/sub_path\n\n" "URL должен быть без слэша и имени клиента на конце!\n" "Его можно увидеть в панели 3x-ui в информации о клиенте.", - parse_mode="HTML", reply_markup=builder.as_markup(), ) await state.set_state(UserEditorState.waiting_for_subscription_url) @@ -225,7 +219,6 @@ async def handle_subscription_url_input(message: types.Message, state: FSMContex if not subscription_url.startswith("https://"): await message.answer( "❌ subscription_url должен начинаться с https://. Попробуйте снова.", - parse_mode="HTML", ) return @@ -244,7 +237,6 @@ async def handle_subscription_url_input(message: types.Message, state: FSMContex await message.answer( f"Введите inbound_id для сервера {server_name} в кластере {cluster_name}:\n\n" "Это номер подключения vless в вашей панели 3x-ui. Обычно это 1 при чистой настройке по гайду.\n\n", - parse_mode="HTML", reply_markup=builder.as_markup(), ) await state.set_state(UserEditorState.waiting_for_inbound_id) @@ -501,7 +493,6 @@ async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMCo f"📡 API URL: {api_url}\n" f"🌐 Subscription URL: {subscription_url}\n" f"🔑 Inbound ID: {inbound_id}", - parse_mode="HTML", reply_markup=builder.as_markup(), ) else: @@ -570,7 +561,6 @@ async def handle_add_server(callback_query: types.CallbackQuery, state: FSMConte f"Введите имя сервера для кластера {cluster_name}:\n\n" "Рекомендуется указать локацию сервера в имени.\n\n" "Пример: server-asia, server-europe", - parse_mode="HTML", reply_markup=builder.as_markup(), ) @@ -598,7 +588,6 @@ async def handle_backup_cluster(callback_query: types.CallbackQuery): await callback_query.message.answer( f"Бэкап для кластера {cluster_name} был успешно создан и отправлен администраторам!\n\n" f"🔔 Бэкапы отправлены в боты панелей.", - parse_mode="HTML", reply_markup=builder.as_markup(), ) await callback_query.answer() diff --git a/handlers/coupons.py b/handlers/coupons.py index 15b3aa55..eb0f7d4c 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -41,7 +41,7 @@ async def process_coupon_code(message: types.Message, state: FSMContext, session builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await message.answer( - activation_result, reply_markup=builder.as_markup(), parse_mode="HTML" + activation_result, reply_markup=builder.as_markup() ) await state.clear() diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index 215b1452..a77e63ba 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -95,7 +95,6 @@ async def process_connect_pc(callback_query: types.CallbackQuery, session: Any): await callback_query.message.answer( instruction_message, reply_markup=builder.as_markup(), - parse_mode="HTML", disable_web_page_preview=True, ) @@ -115,7 +114,6 @@ async def process_connect_tv(callback_query: types.CallbackQuery): await callback_query.message.answer( text=CONNECT_TV_TEXT, reply_markup=builder.as_markup(), - parse_mode="HTML", disable_web_page_preview=True, ) @@ -156,5 +154,5 @@ async def process_continue_tv(callback_query: types.CallbackQuery): builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await callback_query.message.answer( - text=message_text, reply_markup=builder.as_markup(), parse_mode="HTML" + text=message_text, reply_markup=builder.as_markup() ) diff --git a/handlers/utils.py b/handlers/utils.py index b190e158..c482ff7f 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -106,7 +106,7 @@ async def handle_error( except Exception as delete_error: logger.warning(f"Не удалось удалить сообщение: {delete_error}") - await bot.send_message(tg_id, message, parse_mode="HTML") + await bot.send_message(tg_id, message) except Exception as e: logger.error(f"Ошибка при обработке ошибки: {e}") diff --git a/servers.py b/servers.py index eacfc9e9..a5920cc6 100644 --- a/servers.py +++ b/servers.py @@ -132,7 +132,6 @@ async def notify_admin(server_name: str): "Проверьте соединение к серверу, подключение к панели или удалите его из таблицы серверов в боте, " "чтобы не выдать подписку к неработающему серверу." ), - parse_mode="HTML", reply_markup=builder.as_markup(), ) logger.info( From 4b436a3d1212bdd355925a8f3f8fea1f143fd7fa Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Wed, 15 Jan 2025 02:39:01 +0300 Subject: [PATCH 02/49] Add page coupons --- database.py | 39 +++++++++++++++++++++--------- handlers/admin/admin_coupons.py | 43 +++++++++++++++++---------------- 2 files changed, 50 insertions(+), 32 deletions(-) diff --git a/database.py b/database.py index c7caa99e..92db1b6e 100644 --- a/database.py +++ b/database.py @@ -114,35 +114,52 @@ async def create_coupon( raise -async def get_all_coupons(session: Any): +async def get_all_coupons(session: Any, page: int = 1, per_page: int = 10): """ - Получает список всех купонов из базы данных. + Получает список купонов из базы данных с пагинацией. + + Args: + session (Any): Сессия базы данных для выполнения запроса + page (int): Номер страницы (по умолчанию 1) + per_page (int): Количество купонов на странице (по умолчанию 10) Returns: - list: Список словарей с информацией о купонах, каждый словарь содержит: - - code (str): Код купона - - amount (int): Сумма купона - - usage_limit (int): Максимальное количество использований - - usage_count (int): Текущее количество использований купона + dict: Словарь с информацией о купонах и пагинации: + - coupons (list): Список словарей с информацией о купонах + - total (int): Общее количество купонов + - pages (int): Общее количество страниц + - current_page (int): Текущая страница Raises: Exception: В случае ошибки при получении данных из базы """ try: + offset = (page - 1) * per_page coupons = await session.fetch( """ SELECT code, amount, usage_limit, usage_count FROM coupons - """ + ORDER BY id + LIMIT $1 OFFSET $2 + """, + per_page, offset ) - logger.info(f"Успешно получено {len(coupons)} купонов из базы данных") + total_count = await session.fetchval("SELECT COUNT(*) FROM coupons") + total_pages = -(-total_count // per_page) # Округление вверх - return coupons + logger.info(f"Успешно получено {len(coupons)} купонов из базы данных (страница {page})") + + return { + "coupons": coupons, + "total": total_count, + "pages": total_pages, + "current_page": page + } except Exception as e: logger.error(f"Критическая ошибка при получении списка купонов: {e}") logger.exception("Трассировка стека ошибки получения купонов") - return [] + return {"coupons": [], "total": 0, "pages": 0, "current_page": page} async def delete_coupon_from_db(coupon_code: str, session: Any): diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index 27e3071e..f060acb7 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -27,31 +27,33 @@ async def show_coupon_management_menu( builder.row( InlineKeyboardButton(text="➕ Создать купон", callback_data="create_coupon") ) - builder.row(InlineKeyboardButton(text="Купоны", callback_data="coupons")) + builder.row(InlineKeyboardButton(text="🎟️ Купоны", callback_data="coupons")) builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) await callback_query.message.answer( "🛠 Меню управления купонами:", reply_markup=builder.as_markup() ) -@router.callback_query(F.data == "coupons", IsAdminFilter()) +@router.callback_query(F.data.startswith("coupons"), IsAdminFilter()) async def show_coupon_list(callback_query: types.CallbackQuery, session: Any): try: - coupons = await get_all_coupons(session) + page = int(callback_query.data.split(':')[1]) if ':' in callback_query.data else 1 + per_page = 10 + result = await get_all_coupons(session, page, per_page) + coupons = result["coupons"] + total_pages = result["pages"] + current_page = result["current_page"] if not coupons: builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor") - ) - + builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) await callback_query.message.answer( "❌ На данный момент нет доступных купонов. 🚫\nВы можете вернуться в меню управления. 🔙", reply_markup=builder.as_markup(), ) return - coupon_list = "📜 Список всех купонов:\n\n" + coupon_list = f"📜 Список купонов (страница {current_page} из {total_pages}):\n\n" builder = InlineKeyboardBuilder() for coupon in coupons: @@ -61,23 +63,22 @@ async def show_coupon_list(callback_query: types.CallbackQuery, session: Any): f"🔢 Лимит использования: {coupon['usage_limit']} раз\n" f"✅ Использовано: {coupon['usage_count']} раз\n\n" ) + builder.row(InlineKeyboardButton( + text=f"❌ Удалить {coupon['code']}", + callback_data=f"delete_coupon_{coupon['code']}" + )) - builder.row( - InlineKeyboardButton( - text=f"❌ Удалить {coupon['code']}", - callback_data=f"delete_coupon_{coupon['code']}", - ) - ) - - builder.row( - InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor") - ) - await callback_query.message.answer( - coupon_list, reply_markup=builder.as_markup() - ) + if current_page > 1: + builder.row(InlineKeyboardButton(text="⬅️ Предыдущая", callback_data=f"coupons:{current_page-1}")) + if current_page < total_pages: + builder.row(InlineKeyboardButton(text="➡️ Следующая", callback_data=f"coupons:{current_page+1}")) + + builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) + await callback_query.message.answer(coupon_list, reply_markup=builder.as_markup()) except Exception as e: logger.error(f"Ошибка при получении списка купонов: {e}") + await callback_query.message.answer("Произошла ошибка при получении списка купонов.") @router.callback_query(F.data.startswith("delete_coupon_"), IsAdminFilter()) From 260618dcc2621cef3df59b1c116258ee95ca3c17 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Fri, 17 Jan 2025 01:49:24 +0300 Subject: [PATCH 03/49] Update backlog --- handlers/keys/keys.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 8a270a7d..65f3ba6d 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -78,7 +78,7 @@ async def process_callback_or_message_view_keys( try: records = await session.fetch( """ - SELECT email, client_id FROM keys WHERE tg_id = $1 + SELECT email, client_id, expiry_time FROM keys WHERE tg_id = $1 """, chat_id, ) @@ -103,9 +103,11 @@ def build_keys_response(records): if records: for record in records: key_name = record["email"] + expiry_date = datetime.utcfromtimestamp(record["expiry_time"] / 1000).strftime("%d.%m.%Y") builder.row( InlineKeyboardButton( - text=f"🔑 {key_name}", callback_data=f"view_key|{key_name}" + text=f"🔑 {key_name} (до {expiry_date})", + callback_data=f"view_key|{key_name}" ) ) @@ -235,7 +237,11 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session text="⏳ Продлить", callback_data=f"renew_key|{key_name}" ) ) - + builder.row( + InlineKeyboardButton( + text="🔙 Назад", callback_data="view_keys" + ) + ) builder.row( InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") ) From c4eec4572ec1d6538535dd6c1fe310b363bbd997 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 19 Jan 2025 08:29:10 +0300 Subject: [PATCH 04/49] Add filter and middleware --- bot.py | 9 ++++-- filters/private.py | 8 +++++ middlewares/throttling.py | 61 ++++++++++++++++++++++++++++++++++----- 3 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 filters/private.py diff --git a/bot.py b/bot.py index 715fb7b2..7a708d80 100644 --- a/bot.py +++ b/bot.py @@ -13,6 +13,8 @@ from middlewares.database import DatabaseMiddleware from middlewares.delete import DeleteMessageMiddleware from middlewares.logging import LoggingMiddleware from middlewares.user import UserMiddleware +from middlewares.throttling import ThrottlingMiddleware +from filters.private import IsPrivate bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) storage = MemoryStorage() @@ -29,12 +31,15 @@ dp.callback_query.middleware(UserMiddleware()) dp.message.middleware(DatabaseMiddleware()) dp.callback_query.middleware(DatabaseMiddleware()) -# dp.message.middleware(ThrottlingMiddleware(limit=1)) -# dp.callback_query.middleware(ThrottlingMiddleware(limit=1)) +dp.message.middleware(ThrottlingMiddleware()) +dp.callback_query.middleware(ThrottlingMiddleware()) dp.message.outer_middleware(DeleteMessageMiddleware()) dp.callback_query.outer_middleware(DeleteMessageMiddleware()) +dp.message.filter(IsPrivate()) +dp.callback_query.filter(IsPrivate()) + @dp.error() async def error_handler(event: ErrorEvent): diff --git a/filters/private.py b/filters/private.py new file mode 100644 index 00000000..988db95c --- /dev/null +++ b/filters/private.py @@ -0,0 +1,8 @@ +from aiogram.enums import ChatType +from aiogram.filters import BaseFilter +from aiogram.types import Chat, TelegramObject + + +class IsPrivate(BaseFilter): + async def __call__(self, event: TelegramObject, event_chat: Chat) -> bool: + return event_chat.type == ChatType.PRIVATE \ No newline at end of file diff --git a/middlewares/throttling.py b/middlewares/throttling.py index 2761920c..af753651 100644 --- a/middlewares/throttling.py +++ b/middlewares/throttling.py @@ -1,13 +1,29 @@ -from collections.abc import Awaitable, Callable -from typing import Any +from typing import Any, Awaitable, Callable, MutableMapping from aiogram import BaseMiddleware -from aiogram.types import TelegramObject +from aiogram.dispatcher.flags import get_flag +from aiogram.types import TelegramObject, Update, User +from cachetools import TTLCache +from logger import logger -class ThrottleMiddleware(BaseMiddleware): - def __init__(self, limit: int): - self.limit = limit +class ThrottlingMiddleware(BaseMiddleware): + def __init__( + self, + *, + default_key: str | None = "default", + default_ttl: float = 0.5, + **ttl_map: float, + ) -> None: + if default_key: + ttl_map[default_key] = default_ttl + + self.default_key = default_key + self.caches: dict[str, MutableMapping[int, None]] = {} + + for name, ttl in ttl_map.items(): + self.caches[name] = TTLCache(maxsize=10_000, ttl=ttl) + logger.debug("ThrottlingMiddleware initialized.") async def __call__( self, @@ -15,5 +31,34 @@ class ThrottleMiddleware(BaseMiddleware): event: TelegramObject, data: dict[str, Any], ) -> Any: - #todo - return await handler(event, data) + if not isinstance(event, Update): + logger.debug(f"Received event of type {type(event)}, skipping throttling.") + return await handler(event, data) + + if event.pre_checkout_query: + logger.debug("Pre-checkout query event, skipping throttling.") + return await handler(event, data) + + if event.message and event.message.successful_payment: + logger.debug("Successful payment event, skipping throttling.") + return await handler(event, data) + + user: User | None = data.get("event_from_user", None) + + if user is not None: + key = get_flag(data, "throttling_key", default=self.default_key) + + if key: + if user.id in self.caches[key]: + logger.warning(f"User {user.id} is being throttled with key: {key}") + return None + logger.debug( + f"User {user.id} is allowed to proceed, adding to cache with key: {key}", + ) + self.caches[key][user.id] = None + else: + logger.debug( + f"No throttling key provided for user {user.id}, proceeding without throttle." + ) + + return await handler(event, data) \ No newline at end of file From 689ba3579a50aa1c17f10094ee0dd4c84deb2ed4 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 19 Jan 2025 08:36:18 +0300 Subject: [PATCH 05/49] Update requirements.txt --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3275ecfa..e5e3393f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,4 +32,5 @@ sqlalchemy robokassa ping3 ruff -pytz \ No newline at end of file +pytz +cachetools \ No newline at end of file From cbf53f0a509e52578b2dd8bedf24f5092f81dbc8 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 19 Jan 2025 08:49:46 +0300 Subject: [PATCH 06/49] Small fix --- bot.py | 4 ++-- filters/private.py | 2 +- handlers/admin/admin_coupons.py | 2 +- handlers/keys/keys.py | 2 +- handlers/notifications.py | 4 ++-- middlewares/throttling.py | 6 ++++-- requirements.txt | 3 ++- 7 files changed, 13 insertions(+), 10 deletions(-) diff --git a/bot.py b/bot.py index 7a708d80..a995ed69 100644 --- a/bot.py +++ b/bot.py @@ -7,14 +7,14 @@ from aiogram.fsm.storage.memory import MemoryStorage from aiogram.types import ErrorEvent from config import API_TOKEN +from filters.private import IsPrivate from logger import logger from middlewares.admin import AdminMiddleware from middlewares.database import DatabaseMiddleware from middlewares.delete import DeleteMessageMiddleware from middlewares.logging import LoggingMiddleware -from middlewares.user import UserMiddleware from middlewares.throttling import ThrottlingMiddleware -from filters.private import IsPrivate +from middlewares.user import UserMiddleware bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) storage = MemoryStorage() diff --git a/filters/private.py b/filters/private.py index 988db95c..334da487 100644 --- a/filters/private.py +++ b/filters/private.py @@ -5,4 +5,4 @@ from aiogram.types import Chat, TelegramObject class IsPrivate(BaseFilter): async def __call__(self, event: TelegramObject, event_chat: Chat) -> bool: - return event_chat.type == ChatType.PRIVATE \ No newline at end of file + return event_chat.type == ChatType.PRIVATE diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index f060acb7..9af0c575 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -72,7 +72,7 @@ async def show_coupon_list(callback_query: types.CallbackQuery, session: Any): builder.row(InlineKeyboardButton(text="⬅️ Предыдущая", callback_data=f"coupons:{current_page-1}")) if current_page < total_pages: builder.row(InlineKeyboardButton(text="➡️ Следующая", callback_data=f"coupons:{current_page+1}")) - + builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) await callback_query.message.answer(coupon_list, reply_markup=builder.as_markup()) diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 65f3ba6d..8a9cbbb7 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -106,7 +106,7 @@ def build_keys_response(records): expiry_date = datetime.utcfromtimestamp(record["expiry_time"] / 1000).strftime("%d.%m.%Y") builder.row( InlineKeyboardButton( - text=f"🔑 {key_name} (до {expiry_date})", + text=f"🔑 {key_name} (до {expiry_date})", callback_data=f"view_key|{key_name}" ) ) diff --git a/handlers/notifications.py b/handlers/notifications.py index d5862e9d..cc3cc46c 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -1,8 +1,8 @@ import asyncio from datetime import datetime, timedelta -import pytz import asyncpg +import pytz from aiogram import Bot, Router, types from aiogram.exceptions import TelegramForbiddenError from aiogram.utils.keyboard import InlineKeyboardBuilder @@ -15,10 +15,10 @@ from config import ( AUTO_RENEW_KEYS, DATABASE_URL, DEV_MODE, + EXPIRED_KEYS_CHECK_INTERVAL, RENEWAL_PLANS, TOTAL_GB, TRIAL_TIME, - EXPIRED_KEYS_CHECK_INTERVAL ) from database import ( add_blocked_user, diff --git a/middlewares/throttling.py b/middlewares/throttling.py index af753651..8fd18052 100644 --- a/middlewares/throttling.py +++ b/middlewares/throttling.py @@ -1,4 +1,5 @@ -from typing import Any, Awaitable, Callable, MutableMapping +from collections.abc import Awaitable, Callable, MutableMapping +from typing import Any from aiogram import BaseMiddleware from aiogram.dispatcher.flags import get_flag @@ -7,6 +8,7 @@ from cachetools import TTLCache from logger import logger + class ThrottlingMiddleware(BaseMiddleware): def __init__( self, @@ -61,4 +63,4 @@ class ThrottlingMiddleware(BaseMiddleware): f"No throttling key provided for user {user.id}, proceeding without throttle." ) - return await handler(event, data) \ No newline at end of file + return await handler(event, data) diff --git a/requirements.txt b/requirements.txt index e5e3393f..2d8df8b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,4 +33,5 @@ robokassa ping3 ruff pytz -cachetools \ No newline at end of file +cachetools +babel \ No newline at end of file From dc9c33c1f5f3eae698fe5935b02505e2815b1c5c Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 19 Jan 2025 08:59:43 +0300 Subject: [PATCH 07/49] Update errors_handler --- bot.py | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/bot.py b/bot.py index a995ed69..2935fad1 100644 --- a/bot.py +++ b/bot.py @@ -3,10 +3,13 @@ import traceback from aiogram import Bot, Dispatcher from aiogram.client.default import DefaultBotProperties from aiogram.enums import ParseMode +from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError +from aiogram.filters import ExceptionTypeFilter from aiogram.fsm.storage.memory import MemoryStorage -from aiogram.types import ErrorEvent +from aiogram.types import BufferedInputFile, ErrorEvent +from aiogram.utils.markdown import hbold -from config import API_TOKEN +from config import ADMIN_ID, API_TOKEN from filters.private import IsPrivate from logger import logger from middlewares.admin import AdminMiddleware @@ -41,12 +44,29 @@ dp.message.filter(IsPrivate()) dp.callback_query.filter(IsPrivate()) -@dp.error() -async def error_handler(event: ErrorEvent): - logger.error( - "Ошибка в боте:\n" - f"Исключение: {event.exception}\n" - f"Тип: {type(event.exception)}\n" - f"Update: {event.update}\n" - f"Трассировка:\n{traceback.format_exc()}" - ) +@router.errors(ExceptionTypeFilter(Exception)) +async def errors_handler( + event: ErrorEvent, + bot: Bot, +) -> bool: + if isinstance(event.exception, TelegramForbiddenError): + logger.info(f"User {event.update.message.from_user.id} blocked the bot.") + return True + logger.exception(f"Update: {event.update}\nException: {event.exception}") + if not ADMIN_ID: + return True + try: + for admin_id in ADMIN_ID: + await bot.send_document( + chat_id=admin_id, + document=BufferedInputFile( + traceback.format_exc().encode(), + filename=f"error_{event.update.update_id}.txt", + ), + caption=f"{hbold(type(event.exception).__name__)}: {str(event.exception)[:1021]}...", + ) + except TelegramBadRequest as exception: + logger.warning(f"Failed to send error details: {exception}") + except Exception as exception: + logger.error(f"Unexpected error in error handler: {exception}") + return True From 82a2e634618ddb9d09d9b802384b57e1b57b3401 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 19 Jan 2025 09:15:42 +0300 Subject: [PATCH 08/49] Fix and update Middleware --- bot.py | 24 ++---------------------- filters/private.py | 2 +- middlewares/__init__.py | 24 ++++++++++++++++++++++++ middlewares/{logging.py => loggings.py} | 0 middlewares/{database.py => session.py} | 2 +- 5 files changed, 28 insertions(+), 24 deletions(-) create mode 100644 middlewares/__init__.py rename middlewares/{logging.py => loggings.py} (100%) rename middlewares/{database.py => session.py} (93%) diff --git a/bot.py b/bot.py index 2935fad1..9ca8a11f 100644 --- a/bot.py +++ b/bot.py @@ -12,33 +12,13 @@ from aiogram.utils.markdown import hbold from config import ADMIN_ID, API_TOKEN from filters.private import IsPrivate from logger import logger -from middlewares.admin import AdminMiddleware -from middlewares.database import DatabaseMiddleware -from middlewares.delete import DeleteMessageMiddleware -from middlewares.logging import LoggingMiddleware -from middlewares.throttling import ThrottlingMiddleware -from middlewares.user import UserMiddleware +from middlewares import register_middleware bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) storage = MemoryStorage() dp = Dispatcher(bot=bot, storage=storage) -dp.message.middleware(LoggingMiddleware()) -dp.callback_query.middleware(LoggingMiddleware()) - -dp.message.middleware(AdminMiddleware()) -dp.callback_query.middleware(AdminMiddleware()) -dp.message.middleware(UserMiddleware()) -dp.callback_query.middleware(UserMiddleware()) - -dp.message.middleware(DatabaseMiddleware()) -dp.callback_query.middleware(DatabaseMiddleware()) - -dp.message.middleware(ThrottlingMiddleware()) -dp.callback_query.middleware(ThrottlingMiddleware()) - -dp.message.outer_middleware(DeleteMessageMiddleware()) -dp.callback_query.outer_middleware(DeleteMessageMiddleware()) +register_middleware(dp) dp.message.filter(IsPrivate()) dp.callback_query.filter(IsPrivate()) diff --git a/filters/private.py b/filters/private.py index 334da487..6dea490e 100644 --- a/filters/private.py +++ b/filters/private.py @@ -3,6 +3,6 @@ from aiogram.filters import BaseFilter from aiogram.types import Chat, TelegramObject -class IsPrivate(BaseFilter): +class IsPrivateFilter(BaseFilter): async def __call__(self, event: TelegramObject, event_chat: Chat) -> bool: return event_chat.type == ChatType.PRIVATE diff --git a/middlewares/__init__.py b/middlewares/__init__.py new file mode 100644 index 00000000..66397ae5 --- /dev/null +++ b/middlewares/__init__.py @@ -0,0 +1,24 @@ +from typing import Any + +from admin import AdminMiddleware +from aiogram import Dispatcher +from delete import DeleteMessageMiddleware +from loggings import LoggingMiddleware +from session import SessionMiddleware +from throttling import ThrottlingMiddleware +from user import UserMiddleware + + +def register_middleware(dispatcher: Dispatcher) -> None: + middlewares = [ + AdminMiddleware(), + SessionMiddleware(), + DeleteMessageMiddleware(), + LoggingMiddleware(), + ThrottlingMiddleware(), + UserMiddleware(), + ] + + for middleware in middlewares: + dispatcher.message.outer_middleware(middleware) + dispatcher.callback_query.outer_middleware(middleware) diff --git a/middlewares/logging.py b/middlewares/loggings.py similarity index 100% rename from middlewares/logging.py rename to middlewares/loggings.py diff --git a/middlewares/database.py b/middlewares/session.py similarity index 93% rename from middlewares/database.py rename to middlewares/session.py index c9bf6906..a5d34441 100644 --- a/middlewares/database.py +++ b/middlewares/session.py @@ -8,7 +8,7 @@ from aiogram.types import TelegramObject from config import DATABASE_URL -class DatabaseMiddleware(BaseMiddleware): +class SessionMiddleware(BaseMiddleware): async def __call__( self, handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], From f07b46ac75fd1eb090404f076afaf1e0013e5d57 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sun, 19 Jan 2025 09:16:56 +0300 Subject: [PATCH 09/49] Fix errors_handler --- bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot.py b/bot.py index 9ca8a11f..dc5423ce 100644 --- a/bot.py +++ b/bot.py @@ -24,7 +24,7 @@ dp.message.filter(IsPrivate()) dp.callback_query.filter(IsPrivate()) -@router.errors(ExceptionTypeFilter(Exception)) +@dp.errors(ExceptionTypeFilter(Exception)) async def errors_handler( event: ErrorEvent, bot: Bot, From b01e4db2b2f33209e7e2caf96d015bbb8b13be99 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Mon, 20 Jan 2025 03:47:16 +0300 Subject: [PATCH 10/49] Update work from blocked_users --- handlers/__init__.py | 2 ++ handlers/notifications.py | 22 ---------------------- handlers/user.py | 21 +++++++++++++++++++++ middlewares/__init__.py | 13 +++++++------ 4 files changed, 30 insertions(+), 28 deletions(-) create mode 100644 handlers/user.py diff --git a/handlers/__init__.py b/handlers/__init__.py index f984092c..85ac4866 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -12,6 +12,7 @@ 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 .user import router as user_router router = Router(name="handlers_main_router") @@ -26,4 +27,5 @@ router.include_routers( keys_router, instructions_router, admin_router, + user_router, ) diff --git a/handlers/notifications.py b/handlers/notifications.py index cc3cc46c..cdc2263c 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -36,28 +36,6 @@ from logger import logger router = Router() -async def check_users_and_update_blocked(bot: Bot): - conn = None - try: - conn = await asyncpg.connect(DATABASE_URL) - users = await conn.fetch("SELECT tg_id FROM users") - - for user in users: - try: - await bot.send_chat_action(user['tg_id'], "typing") - except (TelegramForbiddenError,Exception): - await conn.execute( - "INSERT INTO blocked_users (tg_id) VALUES ($1) ON CONFLICT (tg_id) DO NOTHING", - user['tg_id'] - ) - logger.info(f"User {user['tg_id']} added to blocked_users") - except Exception as e: - logger.error(f"Error in check_users_and_update_blocked: {e}") - finally: - if conn: - await conn.close() - - async def periodic_expired_keys_check(bot: Bot): """Периодическая проверка истекших ключей с кастомным интервалом.""" while True: diff --git a/handlers/user.py b/handlers/user.py new file mode 100644 index 00000000..5e183db8 --- /dev/null +++ b/handlers/user.py @@ -0,0 +1,21 @@ +from typing import Any + +from aiogram import Router +from aiogram.filters.chat_member_updated import KICKED, MEMBER, ChatMemberUpdatedFilter +from aiogram.types import ChatMemberUpdated + +from logger import logger + +router = Router() + + +@router.my_chat_member(ChatMemberUpdatedFilter(member_status_changed=KICKED)) +async def user_blocked_bot(event: ChatMemberUpdated, session: Any): + logger.info(f"User {event.from_user.id} blocked the bot.") + await session.execute("INSERT INTO blocked_users (tg_id) VALUES ($1) ON CONFLICT (tg_id) DO NOTHING", event.from_user.id) + + +@router.my_chat_member(ChatMemberUpdatedFilter(member_status_changed=MEMBER)) +async def user_unblocked_bot(event: ChatMemberUpdated, session: Any): + logger.info(f"User {event.from_user.id} unblocked the bot.") + await session.execute("DELETE FROM blocked_users WHERE tg_id = $1", event.from_user.id) diff --git a/middlewares/__init__.py b/middlewares/__init__.py index 66397ae5..3257c3bd 100644 --- a/middlewares/__init__.py +++ b/middlewares/__init__.py @@ -1,12 +1,13 @@ from typing import Any -from admin import AdminMiddleware from aiogram import Dispatcher -from delete import DeleteMessageMiddleware -from loggings import LoggingMiddleware -from session import SessionMiddleware -from throttling import ThrottlingMiddleware -from user import UserMiddleware + +from .admin import AdminMiddleware +from .delete import DeleteMessageMiddleware +from .loggings import LoggingMiddleware +from .session import SessionMiddleware +from .throttling import ThrottlingMiddleware +from .user import UserMiddleware def register_middleware(dispatcher: Dispatcher) -> None: From 84831a72900786997481c5c37b398e8c6f22d2e4 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Mon, 20 Jan 2025 22:27:39 +0300 Subject: [PATCH 11/49] Add captcha functionality to start command handler - Included captcha router in main handlers. - Integrated captcha generation in the start command, conditional on CAPTCHA_ENABLE setting. --- handlers/__init__.py | 2 ++ handlers/captcha.py | 76 ++++++++++++++++++++++++++++++++++++++++++++ handlers/start.py | 8 +++++ 3 files changed, 86 insertions(+) create mode 100644 handlers/captcha.py diff --git a/handlers/__init__.py b/handlers/__init__.py index 85ac4866..92c8292a 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -13,11 +13,13 @@ from .payments import router as payments_router from .profile import router as profile_router from .start import router as start_router from .user import router as user_router +from .captcha import router as captcha_router router = Router(name="handlers_main_router") router.include_routers( start_router, + captcha_router, profile_router, pay_router, donate_router, diff --git a/handlers/captcha.py b/handlers/captcha.py new file mode 100644 index 00000000..9f36cf63 --- /dev/null +++ b/handlers/captcha.py @@ -0,0 +1,76 @@ +import random +from typing import Any + +from aiogram import F, Router +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from config import CAPTCHA_EMOJIS +from handlers.start import start_command +from logger import logger + +router = Router() + + +async def generate_captcha(state: FSMContext): + """Генерирует новую капчу и сохраняет правильный ответ в состоянии + + Пример словаря CAPTCHA_EMOJIS: + { + "🐶": "собаку", # Собака + "🐱": "кошку", # Кошка + "🐭": "мышь", # Мышь + "🐹": "хомяка", # Хомяк + "🐰": "кролика", # Кролик + ... + } + """ + # Выбираем случайный эмодзи и его описание из конфига + correct_emoji, correct_text = random.choice(list(CAPTCHA_EMOJIS.items())) + + # Получаем 3 случайных неправильных эмодзи + wrong_emojis = random.sample( + [e for e in CAPTCHA_EMOJIS.keys() if e != correct_emoji], 3 + ) + + # Создаем список всех эмодзи и перемешиваем их + all_emojis = [correct_emoji] + wrong_emojis + random.shuffle(all_emojis) + + # Сохраняем правильный ответ в состоянии + await state.update_data(correct_emoji=correct_emoji) + + # Создаем клавиатуру + builder = InlineKeyboardBuilder() + for emoji in all_emojis: + builder.button(text=emoji, callback_data=f"captcha_{emoji}") + builder.adjust(2, 2) + + return { + "text": f"🔒 Для подтверждения что вы не робот,\nвыберите кнопку с {correct_text}", + "markup": builder.as_markup() + } + + +@router.callback_query(F.data.startswith("captcha_")) +async def check_captcha( + callback: CallbackQuery, state: FSMContext, session: Any, admin: bool +): + """Проверяет ответ пользователя на капчу""" + selected_emoji = callback.data.split("captcha_")[1] + state_data = await state.get_data() + correct_emoji = state_data.get("correct_emoji") + + if selected_emoji == correct_emoji: + logger.info(f"Пользователь {callback.message.chat.id} успешно прошел капчу") + await callback.message.delete() + await start_command(callback.message, state, session, admin) + else: + logger.warning( + f"Пользователь {callback.message.chat.id} неверно ответил на капчу" + ) + captcha = await generate_captcha(state) + await callback.message.edit_text( + text=captcha["text"], reply_markup=captcha["markup"] + ) diff --git a/handlers/start.py b/handlers/start.py index d9c9ead5..070f57fa 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -21,6 +21,7 @@ from config import ( DOWNLOAD_ANDROID, DOWNLOAD_IOS, SUPPORT_CHAT_URL, + CAPTCHA_ENABLE, ) from database import ( add_connection, @@ -41,6 +42,7 @@ from handlers.keys.key_management import create_key from handlers.keys.trial_key import create_trial_key from handlers.texts import INSTRUCTIONS_TRIAL, WELCOME_TEXT, get_about_vpn from logger import logger +from handlers.captcha import generate_captcha router = Router() @@ -59,6 +61,12 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin await state.clear() + # Проверка капчи, если включена + if CAPTCHA_ENABLE: + captcha = await generate_captcha(state) + await message.answer(text=captcha["text"], reply_markup=captcha["markup"]) + return + if message.text: try: connection_exists = await check_connection_exists(message.chat.id) From 7802af7d419d099cb2b43fca0983dc2c632db8e2 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Mon, 20 Jan 2025 22:31:14 +0300 Subject: [PATCH 12/49] Refactor captcha response handling in check_captcha function - Removed the deletion of the original message upon correct captcha response. - Changed the method of sending the captcha text and markup from edit_text to answer for better user experience. --- handlers/captcha.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/handlers/captcha.py b/handlers/captcha.py index 9f36cf63..c7d5ed16 100644 --- a/handlers/captcha.py +++ b/handlers/captcha.py @@ -64,13 +64,12 @@ async def check_captcha( if selected_emoji == correct_emoji: logger.info(f"Пользователь {callback.message.chat.id} успешно прошел капчу") - await callback.message.delete() await start_command(callback.message, state, session, admin) else: logger.warning( f"Пользователь {callback.message.chat.id} неверно ответил на капчу" ) captcha = await generate_captcha(state) - await callback.message.edit_text( + await callback.message.answer( text=captcha["text"], reply_markup=captcha["markup"] ) From ffc18f4056996bcefdb7cd1c9a8117d40a120008 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Mon, 20 Jan 2025 22:38:52 +0300 Subject: [PATCH 13/49] Refactor private message filter in bot.py - Renamed IsPrivate to IsPrivateFilter for clarity and consistency. - Updated message and callback query filters to use the new IsPrivateFilter class. --- bot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot.py b/bot.py index dc5423ce..99596357 100644 --- a/bot.py +++ b/bot.py @@ -10,7 +10,7 @@ from aiogram.types import BufferedInputFile, ErrorEvent from aiogram.utils.markdown import hbold from config import ADMIN_ID, API_TOKEN -from filters.private import IsPrivate +from filters.private import IsPrivateFilter from logger import logger from middlewares import register_middleware @@ -20,8 +20,8 @@ dp = Dispatcher(bot=bot, storage=storage) register_middleware(dp) -dp.message.filter(IsPrivate()) -dp.callback_query.filter(IsPrivate()) +dp.message.filter(IsPrivateFilter()) +dp.callback_query.filter(IsPrivateFilter()) @dp.errors(ExceptionTypeFilter(Exception)) From acb73101c0f853375fb40cfd5085deeb4c0b232b Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Tue, 21 Jan 2025 08:57:12 +0300 Subject: [PATCH 14/49] Refactor and clean up code formatting across multiple files - Consolidated and simplified code formatting by removing unnecessary line breaks and improving readability in various functions. - Updated the handling of backup file sending in backup.py for better clarity. - Streamlined error handling and logging messages in several handlers to enhance consistency. - Adjusted the Makefile to exclude specific files during Ruff checks and formatting. - Made minor adjustments to function signatures and parameter handling for improved clarity and consistency. This commit enhances code maintainability and readability without altering functionality. --- .github/workflows/format.yml | 35 ++++ Makefile | 7 +- backup.py | 4 +- bot.py | 2 +- client.py | 16 +- database.py | 161 ++++------------ filters/admin.py | 1 - handlers/__init__.py | 2 +- handlers/admin/admin_coupons.py | 31 ++-- handlers/admin/admin_panel.py | 208 +++++---------------- handlers/admin/admin_servers.py | 148 ++++----------- handlers/admin/admin_user_editor.py | 255 ++++++-------------------- handlers/buttons/yookassa.py | 8 +- handlers/captcha.py | 38 ++-- handlers/coupons.py | 8 +- handlers/donate.py | 22 +-- handlers/instructions/instructions.py | 34 +--- handlers/keys/key_management.py | 28 +-- handlers/keys/key_utils.py | 40 +--- handlers/keys/keys.py | 120 ++++-------- handlers/keys/subscriptions.py | 59 ++---- handlers/keys/trial_key.py | 6 +- handlers/notifications.py | 63 +++---- handlers/pay.py | 10 +- handlers/payments/robokassa_pay.py | 47 ++--- handlers/profile.py | 28 +-- handlers/start.py | 108 ++++------- handlers/user.py | 4 +- handlers/utils.py | 12 +- middlewares/delete.py | 4 +- middlewares/throttling.py | 4 +- pyproject.toml | 2 +- servers.py | 55 ++---- 33 files changed, 446 insertions(+), 1124 deletions(-) create mode 100644 .github/workflows/format.yml diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 00000000..410b1bac --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,35 @@ +name: Code Formatting + +on: [push, pull_request] + +jobs: + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff + + - name: Run Ruff formatter with pyproject.toml + run: ruff format . --config pyproject.toml --exclude main.py,handlers/payments + + - name: Check for changes + id: check_changes + run: | + git diff --exit-code || echo "changes_detected=true" >> $GITHUB_OUTPUT + + - name: Commit and push changes + if: steps.check_changes.outputs.changes_detected == 'true' + run: | + git config --global user.name 'GitHub Actions' + git config --global user.email 'actions@github.com' + git add . + git commit -m "Auto-format code with Ruff using pyproject.toml" + git push \ No newline at end of file diff --git a/Makefile b/Makefile index aef0b00f..d949db7d 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ formatting: - @echo "Running Ruff..." && ruff check . --fix - @echo "Running Ruff format..." && ruff format . + @echo "Running Ruff format..." && ruff format . --config pyproject.toml --exclude main.py,handlers/payments + + @echo "Running Ruff..." && ruff check . --config pyproject.toml --exclude main.py,handlers/payments --fix lint: - @echo "Running Ruff checks..." && ruff check . \ No newline at end of file + @echo "Running Ruff checks..." && ruff check . --config pyproject.toml --exclude main.py,handlers/payments \ No newline at end of file diff --git a/backup.py b/backup.py index e44b45f1..c97c4364 100644 --- a/backup.py +++ b/backup.py @@ -59,9 +59,7 @@ def _create_database_backup(): async def _send_backup_to_admin(bot, backup_file_path): try: with open(backup_file_path, "rb") as backup_file: - backup_input_file = BufferedInputFile( - backup_file.read(), filename=os.path.basename(backup_file_path) - ) + backup_input_file = BufferedInputFile(backup_file.read(), filename=os.path.basename(backup_file_path)) admin_ids: int | list[int] = ADMIN_ID if isinstance(admin_ids, list): for id in admin_ids: diff --git a/bot.py b/bot.py index 99596357..006efeb4 100644 --- a/bot.py +++ b/bot.py @@ -44,7 +44,7 @@ async def errors_handler( filename=f"error_{event.update.update_id}.txt", ), caption=f"{hbold(type(event.exception).__name__)}: {str(event.exception)[:1021]}...", - ) + ) except TelegramBadRequest as exception: logger.warning(f"Failed to send error details: {exception}") except Exception as exception: diff --git a/client.py b/client.py index 10953586..f0561bc3 100644 --- a/client.py +++ b/client.py @@ -15,7 +15,7 @@ async def add_client( enable: bool, flow: str, inbound_id: int, - sub_id + sub_id, ): """ Adds a client to the server via 3x-ui. @@ -45,9 +45,7 @@ async def add_client( error_message = str(e) if "Duplicate email" in error_message: - logger.warning( - f"Дублированный email: {email}. Пропуск. Сообщение: {error_message}" - ) + logger.warning(f"Дублированный email: {email}. Пропуск. Сообщение: {error_message}") return {"status": "duplicate", "email": email} logger.error(f"Ошибка при добавлении клиента {email}: {error_message}") @@ -55,7 +53,7 @@ async def add_client( async def extend_client_key( - xui, inbound_id, email: str, new_expiry_time: int, client_id: str, total_gb: int, sub_id = str + xui, inbound_id, email: str, new_expiry_time: int, client_id: str, total_gb: int, sub_id=str ): """ Функция для обновления срока действия ключа клиента по email. @@ -72,9 +70,7 @@ async def extend_client_key( logger.warning(f"Ошибка: клиент {email} не имеет действительного ID.") return - logger.info( - f"Обновление ключа клиента {client.email} с ID {client.id} до нового времени: {new_expiry_time}" - ) + logger.info(f"Обновление ключа клиента {client.email} с ID {client.id} до нового времени: {new_expiry_time}") client.id = client_id client.expiry_time = new_expiry_time @@ -87,9 +83,7 @@ async def extend_client_key( await xui.client.update(client.id, client) await xui.client.reset_stats(inbound_id, email) - logger.info( - f"Ключ клиента {client.email} успешно продлён до {new_expiry_time}." - ) + logger.info(f"Ключ клиента {client.email} успешно продлён до {new_expiry_time}.") except Exception as e: logger.error(f"Ошибка при обновлении клиента с email {email}: {e}") diff --git a/database.py b/database.py index 92db1b6e..a2d1a9ca 100644 --- a/database.py +++ b/database.py @@ -26,9 +26,7 @@ async def save_temporary_data(session, tg_id: int, state: str, data: dict): async def get_temporary_data(session, tg_id: int) -> dict | None: """Извлекает временные данные пользователя.""" - result = await session.fetchrow( - "SELECT state, data FROM temporary_data WHERE tg_id = $1", tg_id - ) + result = await session.fetchrow("SELECT state, data FROM temporary_data WHERE tg_id = $1", tg_id) if result: return {"state": result["state"], "data": json.loads(result["data"])} return None @@ -71,18 +69,14 @@ async def check_unique_server_name(server_name: str) -> bool: """ conn = await asyncpg.connect(DATABASE_URL) - result = await conn.fetchrow( - "SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", server_name - ) + result = await conn.fetchrow("SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", server_name) await conn.close() return result is None -async def create_coupon( - coupon_code: str, amount: float, usage_limit: int, session: Any -): +async def create_coupon(coupon_code: str, amount: float, usage_limit: int, session: Any): """ Создает новый купон в базе данных. @@ -142,7 +136,8 @@ async def get_all_coupons(session: Any, page: int = 1, per_page: int = 10): ORDER BY id LIMIT $1 OFFSET $2 """, - per_page, offset + per_page, + offset, ) total_count = await session.fetchval("SELECT COUNT(*) FROM coupons") @@ -150,12 +145,7 @@ async def get_all_coupons(session: Any, page: int = 1, per_page: int = 10): logger.info(f"Успешно получено {len(coupons)} купонов из базы данных (страница {page})") - return { - "coupons": coupons, - "total": total_count, - "pages": total_pages, - "current_page": page - } + return {"coupons": coupons, "total": total_count, "pages": total_pages, "current_page": page} except Exception as e: logger.error(f"Критическая ошибка при получении списка купонов: {e}") logger.exception("Трассировка стека ошибки получения купонов") @@ -230,9 +220,7 @@ async def restore_trial(tg_id: int, session: Any): logger.info(f"Триальный период успешно восстановлен для пользователя {tg_id}") return True except Exception as e: - logger.error( - f"Ошибка при восстановлении триального периода для пользователя {tg_id}: {e}" - ) + logger.error(f"Ошибка при восстановлении триального периода для пользователя {tg_id}: {e}") return False @@ -264,9 +252,7 @@ async def use_trial(tg_id: int, session: Any): return False -async def add_connection( - tg_id: int, balance: float = 0.0, trial: int = 0, session: Any = None -): +async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0, session: Any = None): """ Добавляет новое подключение для пользователя в базу данных. @@ -293,9 +279,7 @@ async def add_connection( f"Успешно добавлено новое подключение для пользователя {tg_id} с балансом {balance} и статусом триала {trial}" ) except Exception as e: - logger.error( - f"Не удалось добавить подключение для пользователя {tg_id}. Причина: {e}" - ) + logger.error(f"Не удалось добавить подключение для пользователя {tg_id}. Причина: {e}") raise @@ -369,9 +353,7 @@ async def store_key( key, server_id, ) - logger.info( - f"Ключ успешно сохранен для пользователя {tg_id} на сервере {server_id}" - ) + logger.info(f"Ключ успешно сохранен для пользователя {tg_id} на сервере {server_id}") except Exception as e: logger.error(f"Ошибка при сохранении ключа для пользователя {tg_id}: {e}") raise @@ -437,14 +419,10 @@ async def get_keys_by_server(tg_id: int, server_id: str): tg_id, server_id, ) - logger.info( - f"Успешно получено {len(records)} ключей для пользователя {tg_id} на сервере {server_id}" - ) + logger.info(f"Успешно получено {len(records)} ключей для пользователя {tg_id} на сервере {server_id}") return records except Exception as e: - logger.error( - f"Ошибка при получении ключей для пользователя {tg_id} на сервере {server_id}: {e}" - ) + logger.error(f"Ошибка при получении ключей для пользователя {tg_id} на сервере {server_id}: {e}") raise finally: if conn: @@ -468,14 +446,10 @@ async def has_active_key(tg_id: int) -> bool: try: conn = await asyncpg.connect(DATABASE_URL) count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE tg_id = $1", tg_id) - logger.info( - f"Проверка наличия ключей для пользователя {tg_id}. Найдено ключей: {count}" - ) + logger.info(f"Проверка наличия ключей для пользователя {tg_id}. Найдено ключей: {count}") return count > 0 except Exception as e: - logger.error( - f"Ошибка при проверке наличия ключей для пользователя {tg_id}: {e}" - ) + logger.error(f"Ошибка при проверке наличия ключей для пользователя {tg_id}: {e}") raise finally: if conn: @@ -498,9 +472,7 @@ async def get_balance(tg_id: int) -> float: conn = None try: conn = await asyncpg.connect(DATABASE_URL) - balance = await conn.fetchval( - "SELECT balance FROM connections WHERE tg_id = $1", tg_id - ) + balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) logger.info(f"Получен баланс для пользователя {tg_id}: {balance}") return balance if balance is not None else 0.0 except Exception as e: @@ -558,15 +530,11 @@ async def get_trial(tg_id: int, session: Any) -> int: int: Статус триала (0 - не использован, 1 - использован) """ try: - trial = await session.fetchval( - "SELECT trial FROM connections WHERE tg_id = $1", tg_id - ) + trial = await session.fetchval("SELECT trial FROM connections WHERE tg_id = $1", tg_id) logger.info(f"Получен статус триала для пользователя {tg_id}: {trial}") return trial if trial is not None else 0 except Exception as e: - logger.error( - f"Ошибка при получении статуса триала для пользователя {tg_id}: {e}" - ) + logger.error(f"Ошибка при получении статуса триала для пользователя {tg_id}: {e}") return 0 @@ -590,9 +558,7 @@ async def get_key_count(tg_id: int) -> int: logger.info(f"Получено количество ключей для пользователя {tg_id}: {count}") return count if count is not None else 0 except Exception as e: - logger.error( - f"Ошибка при получении количества ключей для пользователя {tg_id}: {e}" - ) + logger.error(f"Ошибка при получении количества ключей для пользователя {tg_id}: {e}") return 0 finally: if conn: @@ -623,11 +589,8 @@ async def get_all_users(conn): async def add_referral(referred_tg_id: int, referrer_tg_id: int, session: Any): try: - if referred_tg_id == referrer_tg_id: - logger.warning( - f"Пользователь {referred_tg_id} попытался использовать свою собственную реферальную ссылку." - ) + logger.warning(f"Пользователь {referred_tg_id} попытался использовать свою собственную реферальную ссылку.") return await session.execute( @@ -638,9 +601,7 @@ async def add_referral(referred_tg_id: int, referrer_tg_id: int, session: Any): referred_tg_id, referrer_tg_id, ) - logger.info( - f"Добавлена реферальная связь: приглашенный {referred_tg_id}, пригласивший {referrer_tg_id}" - ) + logger.info(f"Добавлена реферальная связь: приглашенный {referred_tg_id}, пригласивший {referrer_tg_id}") except Exception as e: logger.error(f"Ошибка при добавлении реферала: {e}") raise @@ -673,9 +634,7 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float): for level in range(1, MAX_REFERRAL_LEVELS + 1): if current_tg_id in visited_tg_ids: - logger.warning( - f"Обнаружен цикл в реферальной цепочке для пользователя {current_tg_id}. Прекращение." - ) + logger.warning(f"Обнаружен цикл в реферальной цепочке для пользователя {current_tg_id}. Прекращение.") break visited_tg_ids.add(current_tg_id) @@ -714,15 +673,11 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float): bonus = round(amount * bonus_percent, 2) if bonus > 0: - logger.info( - f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id} на уровне {level}." - ) + logger.info(f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id} на уровне {level}.") await update_balance(referrer_tg_id, bonus) except Exception as e: - logger.error( - f"Ошибка при обработке многоуровневой реферальной системы для {tg_id}: {e}" - ) + logger.error(f"Ошибка при обработке многоуровневой реферальной системы для {tg_id}: {e}") finally: if conn: await conn.close() @@ -821,9 +776,7 @@ async def get_referral_stats(referrer_tg_id: int): ) total_referral_bonus = total_referral_bonus or 0 - logger.debug( - f"Получена общая сумма бонусов от рефералов: {total_referral_bonus}" - ) + logger.debug(f"Получена общая сумма бонусов от рефералов: {total_referral_bonus}") return { "total_referrals": total_referrals, @@ -833,9 +786,7 @@ async def get_referral_stats(referrer_tg_id: int): } except Exception as e: - logger.error( - f"Ошибка при получении статистики рефералов для пользователя {referrer_tg_id}: {e}" - ) + logger.error(f"Ошибка при получении статистики рефералов для пользователя {referrer_tg_id}: {e}") raise finally: if conn: @@ -857,9 +808,7 @@ async def update_key_expiry(client_id: str, new_expiry_time: int): conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info( - f"Установлено подключение к базе данных для обновления времени истечения ключа клиента {client_id}" - ) + logger.info(f"Установлено подключение к базе данных для обновления времени истечения ключа клиента {client_id}") await conn.execute( """ @@ -873,9 +822,7 @@ async def update_key_expiry(client_id: str, new_expiry_time: int): logger.info(f"Успешно обновлено время истечения ключа для клиента {client_id}") except Exception as e: - logger.error( - f"Ошибка при обновлении времени истечения ключа для клиента {client_id}: {e}" - ) + logger.error(f"Ошибка при обновлении времени истечения ключа для клиента {client_id}: {e}") raise finally: if conn: @@ -896,9 +843,7 @@ async def delete_key(client_id: str): conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info( - f"Установлено подключение к базе данных для удаления ключа клиента {client_id}" - ) + logger.info(f"Установлено подключение к базе данных для удаления ключа клиента {client_id}") await conn.execute( """ @@ -932,9 +877,7 @@ async def add_balance_to_client(client_id: str, amount: float): conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info( - f"Установлено подключение к базе данных для пополнения баланса клиента {client_id}" - ) + logger.info(f"Установлено подключение к базе данных для пополнения баланса клиента {client_id}") await conn.execute( """ @@ -972,9 +915,7 @@ async def get_client_id_by_email(email: str): conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info( - f"Установлено подключение к базе данных для поиска client_id по email: {email}" - ) + logger.info(f"Установлено подключение к базе данных для поиска client_id по email: {email}") client_id = await conn.fetchval( """ @@ -1015,13 +956,9 @@ async def get_tg_id_by_client_id(client_id: str): conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info( - f"Установлено подключение к базе данных для поиска Telegram ID по client_id: {client_id}" - ) + logger.info(f"Установлено подключение к базе данных для поиска Telegram ID по client_id: {client_id}") - result = await conn.fetchrow( - "SELECT tg_id FROM keys WHERE client_id = $1", client_id - ) + result = await conn.fetchrow("SELECT tg_id FROM keys WHERE client_id = $1", client_id) if result: logger.info(f"Найден Telegram ID для client_id: {client_id}") @@ -1064,9 +1001,7 @@ async def upsert_user( conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info( - f"Установлено подключение к базе данных для обновления пользователя {tg_id}" - ) + logger.info(f"Установлено подключение к базе данных для обновления пользователя {tg_id}") await conn.execute( """ @@ -1113,9 +1048,7 @@ async def add_payment(tg_id: int, amount: float, payment_system: str): conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info( - f"Установлено подключение к базе данных для добавления платежа пользователя {tg_id}" - ) + logger.info(f"Установлено подключение к базе данных для добавления платежа пользователя {tg_id}") await conn.execute( """ @@ -1126,9 +1059,7 @@ async def add_payment(tg_id: int, amount: float, payment_system: str): amount, payment_system, ) - logger.info( - f"Успешно добавлен платеж для пользователя {tg_id} на сумму {amount}" - ) + logger.info(f"Успешно добавлен платеж для пользователя {tg_id} на сумму {amount}") except Exception as e: logger.error(f"Ошибка при добавлении платежа для пользователя {tg_id}: {e}") raise @@ -1161,19 +1092,13 @@ async def add_notification(tg_id: int, notification_type: str, session: Any): tg_id, notification_type, ) - logger.info( - f"Успешно добавлено уведомление типа {notification_type} для пользователя {tg_id}" - ) + logger.info(f"Успешно добавлено уведомление типа {notification_type} для пользователя {tg_id}") except Exception as e: - logger.error( - f"Ошибка при добавлении notification для пользователя {tg_id}: {e}" - ) + logger.error(f"Ошибка при добавлении notification для пользователя {tg_id}: {e}") raise -async def check_notification_time( - tg_id: int, notification_type: str, hours: int = 12, session: Any = None -) -> bool: +async def check_notification_time(tg_id: int, notification_type: str, hours: int = 12, session: Any = None) -> bool: """ Проверяет, прошло ли указанное количество часов с момента последнего уведомления. @@ -1218,9 +1143,7 @@ async def check_notification_time( return can_notify except Exception as e: - logger.error( - f"Ошибка при проверке времени уведомления для пользователя {tg_id}: {e}" - ) + logger.error(f"Ошибка при проверке времени уведомления для пользователя {tg_id}: {e}") return False finally: @@ -1259,11 +1182,8 @@ async def get_servers_from_db(): async def delete_user_data(session: Any, tg_id: int): - try: - await session.execute( - "DELETE FROM gifts WHERE sender_tg_id = $1 OR recipient_tg_id = $1", tg_id - ) + await session.execute("DELETE FROM gifts WHERE sender_tg_id = $1 OR recipient_tg_id = $1", tg_id) except Exception as e: logger.warning(f"У Вас версия без подарков для {tg_id}: {e}") await session.execute("DELETE FROM payments WHERE tg_id = $1", tg_id) @@ -1322,7 +1242,6 @@ async def store_gift_link( logger.error(f"Не удалось добавить подарок с ID {gift_id} в базу данных.") return False except Exception as e: - logger.error(f"Ошибка при сохранении подарка с ID {gift_id} в базе данных: {e}") return False diff --git a/filters/admin.py b/filters/admin.py index 95328eaa..5a392217 100644 --- a/filters/admin.py +++ b/filters/admin.py @@ -10,6 +10,5 @@ class IsAdminFilter(BaseFilter): admin_ids: int | list[int] = ADMIN_ID if isinstance(admin_ids, list): return message.from_user.id in admin_ids - return message.from_user.id == admin_ids except Exception: return False diff --git a/handlers/__init__.py b/handlers/__init__.py index 92c8292a..2ac2e777 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -3,6 +3,7 @@ __all__ = ("router",) from aiogram import Router from .admin import router as admin_router +from .captcha import router as captcha_router from .coupons import router as coupons_router from .donate import router as donate_router from .instructions import router as instructions_router @@ -13,7 +14,6 @@ from .payments import router as payments_router from .profile import router as profile_router from .start import router as start_router from .user import router as user_router -from .captcha import router as captcha_router router = Router(name="handlers_main_router") diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index 9af0c575..e83c8fcf 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -19,25 +19,19 @@ router = Router() @router.callback_query(F.data == "coupons_editor", IsAdminFilter()) -async def show_coupon_management_menu( - callback_query: types.CallbackQuery, state: FSMContext -): +async def show_coupon_management_menu(callback_query: types.CallbackQuery, state: FSMContext): await state.clear() builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="➕ Создать купон", callback_data="create_coupon") - ) + builder.row(InlineKeyboardButton(text="➕ Создать купон", callback_data="create_coupon")) builder.row(InlineKeyboardButton(text="🎟️ Купоны", callback_data="coupons")) builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) - await callback_query.message.answer( - "🛠 Меню управления купонами:", reply_markup=builder.as_markup() - ) + await callback_query.message.answer("🛠 Меню управления купонами:", reply_markup=builder.as_markup()) @router.callback_query(F.data.startswith("coupons"), IsAdminFilter()) async def show_coupon_list(callback_query: types.CallbackQuery, session: Any): try: - page = int(callback_query.data.split(':')[1]) if ':' in callback_query.data else 1 + page = int(callback_query.data.split(":")[1]) if ":" in callback_query.data else 1 per_page = 10 result = await get_all_coupons(session, page, per_page) coupons = result["coupons"] @@ -63,15 +57,16 @@ async def show_coupon_list(callback_query: types.CallbackQuery, session: Any): f"🔢 Лимит использования: {coupon['usage_limit']} раз\n" f"✅ Использовано: {coupon['usage_count']} раз\n\n" ) - builder.row(InlineKeyboardButton( - text=f"❌ Удалить {coupon['code']}", - callback_data=f"delete_coupon_{coupon['code']}" - )) + builder.row( + InlineKeyboardButton( + text=f"❌ Удалить {coupon['code']}", callback_data=f"delete_coupon_{coupon['code']}" + ) + ) if current_page > 1: - builder.row(InlineKeyboardButton(text="⬅️ Предыдущая", callback_data=f"coupons:{current_page-1}")) + builder.row(InlineKeyboardButton(text="⬅️ Предыдущая", callback_data=f"coupons:{current_page - 1}")) if current_page < total_pages: - builder.row(InlineKeyboardButton(text="➡️ Следующая", callback_data=f"coupons:{current_page+1}")) + builder.row(InlineKeyboardButton(text="➡️ Следующая", callback_data=f"coupons:{current_page + 1}")) builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) await callback_query.message.answer(coupon_list, reply_markup=builder.as_markup()) @@ -154,9 +149,7 @@ async def process_coupon_data(message: types.Message, state: FSMContext, session ) builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor") - ) + builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) await message.answer(result_message, reply_markup=builder.as_markup()) await state.clear() diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index d6548d6c..1ee47627 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -40,48 +40,23 @@ async def handle_admin_message(message: types.Message, state: FSMContext): BOT_VERSION = "4.0.0-preAlpha(14)" builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="📊 Статистика пользователей", callback_data="user_stats" - ) - ) - builder.row( - InlineKeyboardButton( - text="👥 Управление пользователями", callback_data="user_editor" - ) - ) - builder.row( - InlineKeyboardButton( - text="🖥️ Управление серверами", callback_data="servers_editor" - ) - ) - builder.row( - InlineKeyboardButton( - text="🎟️ Управление купонами", callback_data="coupons_editor" - ) - ) - builder.row( - InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to") - ) - builder.row( - InlineKeyboardButton(text="🤖 Управление Ботом", callback_data="bot_management") - ) + builder.row(InlineKeyboardButton(text="📊 Статистика пользователей", callback_data="user_stats")) + builder.row(InlineKeyboardButton(text="👥 Управление пользователями", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="🖥️ Управление серверами", callback_data="servers_editor")) + builder.row(InlineKeyboardButton(text="🎟️ Управление купонами", callback_data="coupons_editor")) + builder.row(InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to")) + builder.row(InlineKeyboardButton(text="🤖 Управление Ботом", callback_data="bot_management")) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await message.answer( - f"🤖 Панель администратора\n\nВерсия бота: {BOT_VERSION}", - reply_markup=builder.as_markup() + f"🤖 Панель администратора\n\nВерсия бота: {BOT_VERSION}", reply_markup=builder.as_markup() ) @router.callback_query(F.data == "bot_management") async def handle_bot_management(callback_query: types.CallbackQuery): builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups") - ) - builder.row( - InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot") - ) + builder.row(InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups")) + builder.row(InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot")) builder.row(InlineKeyboardButton(text="🚫 Баны", callback_data="ban_user")) builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="admin")) await callback_query.message.answer( @@ -106,13 +81,9 @@ async def user_stats_menu(callback_query: CallbackQuery, session: Any): total_payments_month = await session.fetchval( "SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('month', CURRENT_DATE)" ) - total_payments_all_time = await session.fetchval( - "SELECT COALESCE(SUM(amount), 0) FROM payments" - ) + total_payments_all_time = await session.fetchval("SELECT COALESCE(SUM(amount), 0) FROM payments") - registrations_today = await session.fetchval( - "SELECT COUNT(*) FROM users WHERE created_at >= CURRENT_DATE" - ) + registrations_today = await session.fetchval("SELECT COUNT(*) FROM users WHERE created_at >= CURRENT_DATE") registrations_week = await session.fetchval( "SELECT COUNT(*) FROM users WHERE created_at >= date_trunc('week', CURRENT_DATE)" ) @@ -120,9 +91,7 @@ async def user_stats_menu(callback_query: CallbackQuery, session: Any): "SELECT COUNT(*) FROM users WHERE created_at >= date_trunc('month', CURRENT_DATE)" ) - users_updated_today = await session.fetchval( - "SELECT COUNT(*) FROM users WHERE updated_at >= CURRENT_DATE" - ) + users_updated_today = await session.fetchval("SELECT COUNT(*) FROM users WHERE updated_at >= CURRENT_DATE") active_keys = await session.fetchval( "SELECT COUNT(*) FROM keys WHERE expiry_time > $1", @@ -153,27 +122,17 @@ async def user_stats_menu(callback_query: CallbackQuery, session: Any): ) builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="🔄 Обновить", callback_data="user_stats") - ) + builder.row(InlineKeyboardButton(text="🔄 Обновить", callback_data="user_stats")) builder.row( InlineKeyboardButton( text="📥 Выгрузить пользователей в CSV", callback_data="export_users_csv", ) ) - builder.row( - InlineKeyboardButton( - text="📥 Выгрузить оплаты в CSV", callback_data="export_payments_csv" - ) - ) - builder.row( - InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin") - ) + builder.row(InlineKeyboardButton(text="📥 Выгрузить оплаты в CSV", callback_data="export_payments_csv")) + builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin")) - await callback_query.message.answer( - stats_message, reply_markup=builder.as_markup() - ) + await callback_query.message.answer(stats_message, reply_markup=builder.as_markup()) except Exception as e: logger.error(f"Error in user_stats_menu: {e}") @@ -200,14 +159,10 @@ async def export_users_csv(callback_query: CallbackQuery, session: Any): ) if not users: - await callback_query.message.answer( - "📭 Нет пользователей для экспорта.", reply_markup=builder.as_markup() - ) + await callback_query.message.answer("📭 Нет пользователей для экспорта.", reply_markup=builder.as_markup()) return - csv_data = ( - "tg_id,username,first_name,last_name,language_code,is_bot,balance,trial\n" - ) + csv_data = "tg_id,username,first_name,last_name,language_code,is_bot,balance,trial\n" for user in users: csv_data += f"{user['tg_id']},{user['username']},{user['first_name']},{user['last_name']},{user['language_code']},{user['is_bot']},{user['balance']},{user['trial']}\n" @@ -253,9 +208,7 @@ async def export_payments_csv(callback_query: CallbackQuery, session: Any): ) if not payments: - await callback_query.message.answer( - "📭 Нет платежей для экспорта.", reply_markup=builder.as_markup() - ) + await callback_query.message.answer("📭 Нет платежей для экспорта.", reply_markup=builder.as_markup()) return csv_data = "tg_id,username,first_name,last_name,amount,payment_system,status,created_at\n" # Заголовки CSV @@ -283,24 +236,10 @@ async def export_payments_csv(callback_query: CallbackQuery, session: Any): @router.callback_query(F.data == "send_to", IsAdminFilter()) async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext): builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="📢 Отправить всем", callback_data="send_to_all") - ) - builder.row( - InlineKeyboardButton( - text="📢 Отправить с подпиской", callback_data="send_to_subscribed" - ) - ) - builder.row( - InlineKeyboardButton( - text="📢 Отправить без подписки", callback_data="send_to_unsubscribed" - ) - ) - builder.row( - InlineKeyboardButton( - text="📢 Рассылка по кластеру", callback_data="send_to_cluster" - ) - ) + builder.row(InlineKeyboardButton(text="📢 Отправить всем", callback_data="send_to_all")) + builder.row(InlineKeyboardButton(text="📢 Отправить с подпиской", callback_data="send_to_subscribed")) + builder.row(InlineKeyboardButton(text="📢 Отправить без подписки", callback_data="send_to_unsubscribed")) + builder.row(InlineKeyboardButton(text="📢 Рассылка по кластеру", callback_data="send_to_cluster")) builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) await callback_query.message.answer( "✍️ Выберите группу пользователей и введите текст сообщения для рассылки:", @@ -311,34 +250,26 @@ async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext): @router.callback_query(F.data == "send_to_all", IsAdminFilter()) async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext): await state.update_data(send_to="all") - await callback_query.message.answer( - "✍️ Введите текст сообщения для рассылки всем пользователям:" - ) + await callback_query.message.answer("✍️ Введите текст сообщения для рассылки всем пользователям:") await state.set_state(UserEditorState.waiting_for_message) @router.callback_query(F.data == "send_to_subscribed", IsAdminFilter()) async def handle_send_to_subscribed(callback_query: CallbackQuery, state: FSMContext): await state.update_data(send_to="subscribed") - await callback_query.message.answer( - "✍️ Введите текст сообщения для рассылки пользователям с активной подпиской:" - ) + await callback_query.message.answer("✍️ Введите текст сообщения для рассылки пользователям с активной подпиской:") await state.set_state(UserEditorState.waiting_for_message) @router.callback_query(F.data == "send_to_unsubscribed", IsAdminFilter()) async def handle_send_to_unsubscribed(callback_query: CallbackQuery, state: FSMContext): await state.update_data(send_to="unsubscribed") - await callback_query.message.answer( - "✍️ Введите текст сообщения для рассылки пользователям без активной подписки:" - ) + await callback_query.message.answer("✍️ Введите текст сообщения для рассылки пользователям без активной подписки:") await state.set_state(UserEditorState.waiting_for_message) @router.callback_query(F.data == "send_to_cluster", IsAdminFilter()) -async def handle_send_to_cluster( - callback_query: CallbackQuery, state: FSMContext, session: Any -): +async def handle_send_to_cluster(callback_query: CallbackQuery, state: FSMContext, session: Any): clusters = await session.fetch("SELECT DISTINCT cluster_name FROM servers") builder = InlineKeyboardBuilder() @@ -368,9 +299,7 @@ async def handle_send_cluster(callback_query: CallbackQuery, state: FSMContext): @router.message(UserEditorState.waiting_for_message, IsAdminFilter()) -async def process_message_to_all( - message: types.Message, state: FSMContext, session: Any -): +async def process_message_to_all(message: types.Message, state: FSMContext, session: Any): text_message = message.text try: @@ -424,9 +353,7 @@ async def process_message_to_all( success_count += 1 except Exception as e: error_count += 1 - logger.error( - f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}" - ) + logger.error(f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}") await message.answer( f"📤 Рассылка завершена:\n" @@ -442,13 +369,9 @@ async def process_message_to_all( @router.callback_query(F.data == "backups", IsAdminFilter()) async def handle_backup(callback_query: CallbackQuery, state: FSMContext): - await callback_query.message.answer( - "💾 Инициализация резервного копирования базы данных..." - ) + await callback_query.message.answer("💾 Инициализация резервного копирования базы данных...") await backup_database() - await callback_query.message.answer( - "✅ Резервная копия успешно создана и отправлена администратору." - ) + await callback_query.message.answer("✅ Резервная копия успешно создана и отправлена администратору.") @router.callback_query(F.data == "restart_bot", IsAdminFilter()) @@ -456,9 +379,7 @@ async def handle_restart(callback_query: CallbackQuery, state: FSMContext): await state.set_state(UserEditorState.waiting_for_restart_confirmation) builder = InlineKeyboardBuilder() builder.row( - InlineKeyboardButton( - text="✅ Да, перезапустить", callback_data="confirm_restart" - ), + InlineKeyboardButton(text="✅ Да, перезапустить", callback_data="confirm_restart"), InlineKeyboardButton(text="❌ Нет, отмена", callback_data="admin"), ) builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin")) @@ -484,13 +405,9 @@ async def confirm_restart_bot(callback_query: CallbackQuery, state: FSMContext): text=True, ) await state.clear() - await callback_query.message.answer( - "🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup() - ) + await callback_query.message.answer("🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup()) except subprocess.CalledProcessError: - await callback_query.message.answer( - "🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup() - ) + await callback_query.message.answer("🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup()) except Exception as e: await callback_query.message.answer( f"⚠️ Ошибка при перезагрузке бота: {e.stderr}", @@ -507,37 +424,20 @@ async def user_editor_menu(callback_query: CallbackQuery): callback_data="search_by_key_name", ) ) - builder.row( - InlineKeyboardButton( - text="🆔 Поиск по Telegram ID", callback_data="search_by_tg_id" - ) - ) - builder.row( - InlineKeyboardButton( - text="🌐 Поиск по Username", callback_data="search_by_username" - ) - ) + builder.row(InlineKeyboardButton(text="🆔 Поиск по Telegram ID", callback_data="search_by_tg_id")) + builder.row(InlineKeyboardButton(text="🌐 Поиск по Username", callback_data="search_by_username")) builder.row(InlineKeyboardButton(text="🔙 Вернуться назад", callback_data="admin")) - await callback_query.message.answer( - "👇 Выберите способ поиска пользователя:", reply_markup=builder.as_markup() - ) + await callback_query.message.answer("👇 Выберите способ поиска пользователя:", reply_markup=builder.as_markup()) @router.callback_query(F.data == "ban_user") async def handle_ban_user(callback_query: types.CallbackQuery): builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="📄 Выгрузить в CSV", callback_data="export_to_csv") - ) - builder.row( - InlineKeyboardButton( - text="🗑️ Удалить из БД", callback_data="delete_banned_users" - ) - ) + builder.row(InlineKeyboardButton(text="📄 Выгрузить в CSV", callback_data="export_to_csv")) + builder.row(InlineKeyboardButton(text="🗑️ Удалить из БД", callback_data="delete_banned_users")) builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management")) await callback_query.message.answer( - "🚫 Заблокировавшие бота\n\n" - "Здесь можно просматривать и удалять пользователей, которые забанили вашего бота!", + "🚫 Заблокировавшие бота\n\nЗдесь можно просматривать и удалять пользователей, которые забанили вашего бота!", reply_markup=builder.as_markup(), ) @@ -559,14 +459,10 @@ async def export_banned_users_to_csv(callback_query: types.CallbackQuery): csv_output.seek(0) - document = BufferedInputFile( - file=csv_output.getvalue().encode("utf-8"), filename="banned_users.csv" - ) + document = BufferedInputFile(file=csv_output.getvalue().encode("utf-8"), filename="banned_users.csv") builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management") - ) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management")) await callback_query.message.answer_document( document=document, @@ -575,9 +471,7 @@ async def export_banned_users_to_csv(callback_query: types.CallbackQuery): ) except Exception as e: builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management") - ) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management")) await callback_query.message.answer( text=f"Ошибка при выгрузке CSV: {e}", reply_markup=builder.as_markup(), @@ -594,31 +488,23 @@ async def delete_banned_users(callback_query: types.CallbackQuery): blocked_ids = [record["tg_id"] for record in blocked_users] if not blocked_ids: - await callback_query.message.answer( - "📂 Нет заблокировавших пользователей для удаления." - ) + await callback_query.message.answer("📂 Нет заблокировавших пользователей для удаления.") return for tg_id in blocked_ids: await delete_user_data(conn, tg_id) - await conn.execute( - "DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids - ) + await conn.execute("DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids) builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management") - ) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management")) await callback_query.message.answer( text=f"🗑️ Удалено данные о {len(blocked_ids)} пользователях и связанных записях.", reply_markup=builder.as_markup(), ) except Exception as e: builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management") - ) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management")) await callback_query.message.answer( text=f"Ошибка при удалении записей: {e}", reply_markup=builder.as_markup(), diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index 53792c59..1a039c77 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -9,7 +9,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from py3xui import AsyncApi from backup import create_backup_and_send_to_admins -from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, SUPERNODE +from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL from database import check_unique_server_name, get_servers_from_db from filters.admin import IsAdminFilter from handlers.keys.key_utils import create_key_on_cluster @@ -33,15 +33,9 @@ async def handle_servers_editor(callback_query: types.CallbackQuery): builder = InlineKeyboardBuilder() for cluster_name, cluster_servers in servers.items(): - builder.row( - InlineKeyboardButton( - text=f"⚙️ {cluster_name}", callback_data=f"manage_cluster|{cluster_name}" - ) - ) + builder.row(InlineKeyboardButton(text=f"⚙️ {cluster_name}", callback_data=f"manage_cluster|{cluster_name}")) - builder.row( - InlineKeyboardButton(text="➕ Добавить кластер", callback_data="add_cluster") - ) + builder.row(InlineKeyboardButton(text="➕ Добавить кластер", callback_data="add_cluster")) builder.row(InlineKeyboardButton(text="🔙 Назад в админку", callback_data="admin")) await callback_query.message.answer( @@ -72,11 +66,7 @@ async def handle_cluster_name_input(message: types.Message, state: FSMContext): if cluster_name == "❌ Отменить": await state.clear() builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔧 Управление кластерами", callback_data="servers_editor" - ) - ) + builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor")) await message.answer( "Процесс создания кластера отменен. Вы вернулись в меню управления серверами.", reply_markup=builder.as_markup(), @@ -90,9 +80,7 @@ async def handle_cluster_name_input(message: types.Message, state: FSMContext): await state.update_data(cluster_name=cluster_name) builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") - ) + builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) await message.answer( f"Введите имя сервера для кластера {cluster_name}:\n\n" @@ -110,11 +98,7 @@ async def handle_server_name_input(message: types.Message, state: FSMContext): if server_name == "❌ Отменить": await state.clear() builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔧 Управление кластерами", callback_data="servers_editor" - ) - ) + builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor")) await message.answer( "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", reply_markup=builder.as_markup(), @@ -127,9 +111,7 @@ async def handle_server_name_input(message: types.Message, state: FSMContext): server_unique = await check_unique_server_name(server_name) if not server_unique: - await message.answer( - "❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя." - ) + await message.answer("❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя.") return user_data = await state.get_data() @@ -137,9 +119,7 @@ async def handle_server_name_input(message: types.Message, state: FSMContext): await state.update_data(server_name=server_name) builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") - ) + builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) await message.answer( f"Введите API URL для сервера {server_name} в кластере {cluster_name}:\n\n" @@ -158,11 +138,7 @@ async def handle_api_url_input(message: types.Message, state: FSMContext): if api_url == "❌ Отменить": await state.clear() builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔧 Управление кластерами", callback_data="servers_editor" - ) - ) + builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor")) await message.answer( "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", reply_markup=builder.as_markup(), @@ -183,9 +159,7 @@ async def handle_api_url_input(message: types.Message, state: FSMContext): await state.update_data(api_url=api_url) builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") - ) + builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) await message.answer( f"Введите subscription_url для сервера {server_name} в кластере {cluster_name}:\n\n" @@ -205,11 +179,7 @@ async def handle_subscription_url_input(message: types.Message, state: FSMContex if subscription_url == "❌ Отменить": await state.clear() builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔧 Управление кластерами", callback_data="servers_editor" - ) - ) + builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor")) await message.answer( "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", reply_markup=builder.as_markup(), @@ -230,9 +200,7 @@ async def handle_subscription_url_input(message: types.Message, state: FSMContex await state.update_data(subscription_url=subscription_url) builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") - ) + builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) await message.answer( f"Введите inbound_id для сервера {server_name} в кластере {cluster_name}:\n\n" @@ -247,9 +215,7 @@ async def handle_inbound_id_input(message: types.Message, state: FSMContext): inbound_id = message.text.strip() if not inbound_id.isdigit(): - await message.answer( - "❌ inbound_id должен быть числовым значением. Попробуйте снова." - ) + await message.answer("❌ inbound_id должен быть числовым значением. Попробуйте снова.") return user_data = await state.get_data() @@ -273,11 +239,7 @@ async def handle_inbound_id_input(message: types.Message, state: FSMContext): await conn.close() builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔙 Назад к кластерам", callback_data="servers_editor" - ) - ) + builder.row(InlineKeyboardButton(text="🔙 Назад к кластерам", callback_data="servers_editor")) await message.answer( f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!", @@ -304,11 +266,7 @@ async def handle_manage_cluster(callback_query: types.CallbackQuery, state: FSMC ) ) - builder.row( - InlineKeyboardButton( - text="➕ Добавить сервер", callback_data=f"add_server|{cluster_name}" - ) - ) + builder.row(InlineKeyboardButton(text="➕ Добавить сервер", callback_data=f"add_server|{cluster_name}")) builder.row( InlineKeyboardButton( @@ -331,11 +289,7 @@ async def handle_manage_cluster(callback_query: types.CallbackQuery, state: FSMC ) ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад в управление кластерами", callback_data="servers_editor" - ) - ) + builder.row(InlineKeyboardButton(text="🔙 Назад в управление кластерами", callback_data="servers_editor")) await callback_query.message.answer( f"🔧 Управление серверами для кластера {cluster_name}", @@ -361,11 +315,7 @@ async def sync_cluster_handler(callback_query: types.CallbackQuery): await callback_query.message.answer( f"❌ Нет ключей для синхронизации в кластере {cluster_name}.", reply_markup=InlineKeyboardBuilder() - .row( - InlineKeyboardButton( - text="🔙 Назад", callback_data="servers_editor" - ) - ) + .row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor")) .as_markup(), ) return @@ -421,36 +371,24 @@ async def handle_check_server_availability(callback_query: types.CallbackQuery): "Это может занять до 1 минуты, пожалуйста, подождите..." ) - availability_message = ( - f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n" - ) + availability_message = f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n" for server in cluster_servers: - xui = AsyncApi( - server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD - ) + xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD) try: await xui.login() online_users = len(await xui.client.online()) - availability_message += ( - f"🌍 {server['server_name']}: {online_users} активных пользователей.\n" - ) + availability_message += f"🌍 {server['server_name']}: {online_users} активных пользователей.\n" except Exception as e: availability_message += f"❌ {server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n" builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}" - ) - ) + builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}")) - await in_progress_message.edit_text( - availability_message, reply_markup=builder.as_markup() - ) + await in_progress_message.edit_text(availability_message, reply_markup=builder.as_markup()) await callback_query.answer() @@ -464,9 +402,7 @@ async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMCo server = None cluster_name = None for cluster, cluster_servers in servers.items(): - server = next( - (s for s in cluster_servers if s["server_name"] == server_name), None - ) + server = next((s for s in cluster_servers if s["server_name"] == server_name), None) if server: cluster_name = cluster break @@ -477,16 +413,8 @@ async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMCo inbound_id = server["inbound_id"] builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🗑️ Удалить", callback_data=f"delete_server|{server_name}" - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}" - ) - ) + builder.row(InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"delete_server|{server_name}")) + builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}")) await callback_query.message.answer( f"🔧 Информация о сервере {server_name}:\n\n" @@ -505,12 +433,8 @@ async def handle_delete_server(callback_query: types.CallbackQuery, state: FSMCo builder = InlineKeyboardBuilder() builder.row( - InlineKeyboardButton( - text="✅ Да", callback_data=f"confirm_delete_server|{server_name}" - ), - InlineKeyboardButton( - text="❌ Нет", callback_data=f"manage_server|{server_name}" - ), + InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_delete_server|{server_name}"), + InlineKeyboardButton(text="❌ Нет", callback_data=f"manage_server|{server_name}"), ) await callback_query.message.answer( @@ -520,9 +444,7 @@ async def handle_delete_server(callback_query: types.CallbackQuery, state: FSMCo @router.callback_query(F.data.startswith("confirm_delete_server|"), IsAdminFilter()) -async def handle_confirm_delete_server( - callback_query: types.CallbackQuery, state: FSMContext -): +async def handle_confirm_delete_server(callback_query: types.CallbackQuery, state: FSMContext): server_name = callback_query.data.split("|")[1] conn = await asyncpg.connect(DATABASE_URL) @@ -535,15 +457,9 @@ async def handle_confirm_delete_server( await conn.close() builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔙 Назад в управление кластерами", callback_data="servers_editor" - ) - ) + builder.row(InlineKeyboardButton(text="🔙 Назад в управление кластерами", callback_data="servers_editor")) - await callback_query.message.answer( - f"🗑️ Сервер {server_name} успешно удален.", reply_markup=builder.as_markup() - ) + await callback_query.message.answer(f"🗑️ Сервер {server_name} успешно удален.", reply_markup=builder.as_markup()) @router.callback_query(F.data.startswith("add_server|"), IsAdminFilter()) @@ -553,9 +469,7 @@ async def handle_add_server(callback_query: types.CallbackQuery, state: FSMConte await state.update_data(cluster_name=cluster_name) builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") - ) + builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) await callback_query.message.answer( f"Введите имя сервера для кластера {cluster_name}:\n\n" diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index db2be7ba..94e02f33 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -43,9 +43,7 @@ class UserEditorState(StatesGroup): async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer( - "🔍 Введите Telegram ID клиента:", reply_markup=builder.as_markup() - ) + await callback_query.message.answer("🔍 Введите Telegram ID клиента:", reply_markup=builder.as_markup()) await state.set_state(UserEditorState.waiting_for_tg_id) @@ -53,20 +51,14 @@ async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext): async def prompt_username(callback_query: CallbackQuery, state: FSMContext): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer( - "🔍 Введите Username клиента:", reply_markup=builder.as_markup() - ) + await callback_query.message.answer("🔍 Введите Username клиента:", reply_markup=builder.as_markup()) await state.set_state(UserEditorState.waiting_for_username) @router.message(UserEditorState.waiting_for_username, IsAdminFilter()) -async def handle_username_input( - message: types.Message, state: FSMContext, session: Any -): +async def handle_username_input(message: types.Message, state: FSMContext, session: Any): username = message.text.strip().lstrip("@").replace("https://t.me/", "") - user_record = await session.fetchrow( - "SELECT tg_id FROM users WHERE username = $1", username - ) + user_record = await session.fetchrow("SELECT tg_id FROM users WHERE username = $1", username) if not user_record: builder = InlineKeyboardBuilder() @@ -79,16 +71,10 @@ async def handle_username_input( return tg_id = user_record["tg_id"] - username = await session.fetchval( - "SELECT username FROM users WHERE tg_id = $1", tg_id - ) - balance = await session.fetchval( - "SELECT balance FROM connections WHERE tg_id = $1", tg_id - ) + username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id) + balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) - referral_count = await session.fetchval( - "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id - ) + referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) if balance is None: builder = InlineKeyboardBuilder() @@ -103,9 +89,7 @@ async def handle_username_input( builder = InlineKeyboardBuilder() for (email,) in key_records: - builder.row( - InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}") - ) + builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")) builder.row( InlineKeyboardButton( @@ -120,21 +104,9 @@ async def handle_username_input( callback_data=f"restore_trial_{tg_id}", ) ) - builder.row( - InlineKeyboardButton( - text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}" - ) - ) - builder.row( - InlineKeyboardButton( - text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}" - ) - ) - builder.row( - InlineKeyboardButton( - text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}" - ) - ) + builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}")) + builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}")) + builder.row(InlineKeyboardButton(text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}")) builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) user_info = ( @@ -153,9 +125,7 @@ async def handle_username_input( async def handle_send_message(callback_query: types.CallbackQuery, state: FSMContext): tg_id = callback_query.data.split("_")[2] await state.update_data(target_tg_id=tg_id) - await callback_query.message.answer( - "✉️ Введите текст сообщения, которое вы хотите отправить пользователю." - ) + await callback_query.message.answer("✉️ Введите текст сообщения, которое вы хотите отправить пользователю.") await state.set_state(UserEditorState.waiting_for_message_text) @@ -181,16 +151,10 @@ async def process_send_message(message: types.Message, state: FSMContext, bot: B @router.message(UserEditorState.waiting_for_tg_id, F.text.isdigit(), IsAdminFilter()) async def handle_tg_id_input(message: types.Message, state: FSMContext, session: Any): tg_id = int(message.text) - username = await session.fetchval( - "SELECT username FROM users WHERE tg_id = $1", tg_id - ) - balance = await session.fetchval( - "SELECT balance FROM connections WHERE tg_id = $1", tg_id - ) + username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id) + balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) - referral_count = await session.fetchval( - "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id - ) + referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) if balance is None: builder = InlineKeyboardBuilder() @@ -205,9 +169,7 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext, session: builder = InlineKeyboardBuilder() for (email,) in key_records: - builder.row( - InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}") - ) + builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")) builder.row( InlineKeyboardButton( @@ -215,27 +177,15 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext, session: callback_data=f"change_balance_{tg_id}", ) ) - builder.row( - InlineKeyboardButton( - text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}" - ) - ) - builder.row( - InlineKeyboardButton( - text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}" - ) - ) + builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}")) + builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}")) builder.row( InlineKeyboardButton( text="🔄 Восстановить пробник", callback_data=f"restore_trial_{tg_id}", ) ) - builder.row( - InlineKeyboardButton( - text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}" - ) - ) + builder.row(InlineKeyboardButton(text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}")) builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) @@ -258,15 +208,9 @@ async def handle_restore_trial(callback_query: types.CallbackQuery, session: Any await restore_trial(tg_id, session) builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🔙 Назад в меню администратора", callback_data="admin" - ) - ) + builder.row(InlineKeyboardButton(text="🔙 Назад в меню администратора", callback_data="admin")) - await callback_query.message.answer( - "✅ Триал успешно восстановлен.", reply_markup=builder.as_markup() - ) + await callback_query.message.answer("✅ Триал успешно восстановлен.", reply_markup=builder.as_markup()) @router.callback_query(F.data.startswith("change_balance_"), IsAdminFilter()) @@ -275,16 +219,12 @@ async def process_balance_change(callback_query: CallbackQuery, state: FSMContex await state.update_data(tg_id=tg_id) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer( - "💸 Введите новую сумму баланса:", reply_markup=builder.as_markup() - ) + await callback_query.message.answer("💸 Введите новую сумму баланса:", reply_markup=builder.as_markup()) await state.set_state(UserEditorState.waiting_for_new_balance) @router.message(UserEditorState.waiting_for_new_balance, IsAdminFilter()) -async def handle_new_balance_input( - message: types.Message, state: FSMContext, session: Any -): +async def handle_new_balance_input(message: types.Message, state: FSMContext, session: Any): if not message.text.isdigit() or int(message.text) < 0: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) @@ -318,7 +258,6 @@ async def handle_new_balance_input( async def get_key_details(email, session): - record = await session.fetchrow( """ SELECT k.key, k.expiry_time, k.server_id, c.tg_id, c.balance @@ -399,25 +338,19 @@ async def process_key_edit(callback_query: CallbackQuery, session: Any): ) builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer( - response_message, reply_markup=builder.as_markup() - ) + await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) @router.callback_query(F.data == "search_by_key_name", IsAdminFilter()) async def prompt_key_name(callback_query: CallbackQuery, state: FSMContext): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer( - "🔑 Введите имя ключа:", reply_markup=builder.as_markup() - ) + await callback_query.message.answer("🔑 Введите имя ключа:", reply_markup=builder.as_markup()) await state.set_state(UserEditorState.waiting_for_key_name) @router.message(UserEditorState.waiting_for_key_name, IsAdminFilter()) -async def handle_key_name_input( - message: types.Message, state: FSMContext, session: Any -): +async def handle_key_name_input(message: types.Message, state: FSMContext, session: Any): key_name = sanitize_key_name(message.text) key_details = await get_key_details(key_name, session) @@ -474,18 +407,14 @@ async def prompt_expiry_change(callback_query: CallbackQuery, state: FSMContext) @router.message(UserEditorState.waiting_for_expiry_time, IsAdminFilter()) -async def handle_expiry_time_input( - message: types.Message, state: FSMContext, session: Any -): +async def handle_expiry_time_input(message: types.Message, state: FSMContext, session: Any): user_data = await state.get_data() email = user_data.get("email") if not email: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer( - "📧 Email не найден в состоянии. 🚫", reply_markup=builder.as_markup() - ) + await message.answer("📧 Email не найден в состоянии. 🚫", reply_markup=builder.as_markup()) await state.clear() return @@ -499,9 +428,7 @@ async def handle_expiry_time_input( client_id = await get_client_id_by_email(email) if client_id is None: builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor") - ) + builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) await message.answer( f"🚫 Клиент с email {email} не найден. 🔍", reply_markup=builder.as_markup(), @@ -509,14 +436,10 @@ async def handle_expiry_time_input( await state.clear() return - record = await session.fetchrow( - "SELECT server_id FROM keys WHERE client_id = $1", client_id - ) + record = await session.fetchrow("SELECT server_id FROM keys WHERE client_id = $1", client_id) if not record: builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor") - ) + builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) await message.answer( "🚫 Клиент не найден в базе данных. 🔍", reply_markup=builder.as_markup(), @@ -547,7 +470,9 @@ async def handle_expiry_time_input( await update_key_expiry(client_id, expiry_time) - response_message = f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах." + response_message = ( + f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах." + ) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) @@ -565,20 +490,14 @@ async def handle_expiry_time_input( @router.callback_query(F.data.startswith("delete_key_admin|"), IsAdminFilter()) -async def process_callback_delete_key( - callback_query: types.CallbackQuery, session: Any -): +async def process_callback_delete_key(callback_query: types.CallbackQuery, session: Any): email = callback_query.data.split("|")[1] - client_id = await session.fetchval( - "SELECT client_id FROM keys WHERE email = $1", email - ) + client_id = await session.fetchval("SELECT client_id FROM keys WHERE email = $1", email) if client_id is None: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer( - "🔍 Ключ не найден. 🚫", reply_markup=builder.as_markup() - ) + await callback_query.message.answer("🔍 Ключ не найден. 🚫", reply_markup=builder.as_markup()) return builder = InlineKeyboardBuilder() @@ -588,9 +507,7 @@ async def process_callback_delete_key( callback_data=f"confirm_delete_admin|{client_id}", ) ) - builder.row( - types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="user_editor") - ) + builder.row(types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="user_editor")) await callback_query.message.answer( "❓ Вы уверены, что хотите удалить ключ?", reply_markup=builder.as_markup(), @@ -598,13 +515,9 @@ async def process_callback_delete_key( @router.callback_query(F.data.startswith("confirm_delete_admin|"), IsAdminFilter()) -async def process_callback_confirm_delete( - callback_query: types.CallbackQuery, session: Any -): +async def process_callback_confirm_delete(callback_query: types.CallbackQuery, session: Any): client_id = callback_query.data.split("|")[1] - record = await session.fetchrow( - "SELECT email FROM keys WHERE client_id = $1", client_id - ) + record = await session.fetchrow("SELECT email FROM keys WHERE client_id = $1", client_id) if record: email = record["email"] @@ -618,74 +531,38 @@ async def process_callback_confirm_delete( tasks = [] for cluster_name, cluster_servers in clusters.items(): for server in cluster_servers: - tasks.append( - delete_key_from_cluster(cluster_name, email, client_id) - ) + tasks.append(delete_key_from_cluster(cluster_name, email, client_id)) await asyncio.gather(*tasks) await delete_key_from_servers(email, client_id) await delete_key_from_db(client_id, session) - await callback_query.message.answer( - response_message, reply_markup=builder.as_markup() - ) + await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) else: response_message = "🚫 Ключ не найден или уже удален." builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys")) - await callback_query.message.answer( - response_message, reply_markup=builder.as_markup() - ) + await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) @router.callback_query(F.data.startswith("user_info|"), IsAdminFilter()) -async def handle_user_info( - callback_query: types.CallbackQuery, state: FSMContext, session: Any -): +async def handle_user_info(callback_query: types.CallbackQuery, state: FSMContext, session: Any): tg_id = int(callback_query.data.split("|")[1]) - username = await session.fetchval( - "SELECT username FROM users WHERE tg_id = $1", tg_id - ) - balance = await session.fetchval( - "SELECT balance FROM connections WHERE tg_id = $1", tg_id - ) + username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id) + balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) - referral_count = await session.fetchval( - "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id - ) + referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) builder = InlineKeyboardBuilder() for (email,) in key_records: - builder.row( - InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}") - ) + builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")) - builder.row( - InlineKeyboardButton( - text="📝 Изменить баланс", callback_data=f"change_balance_{tg_id}" - ) - ) - builder.row( - InlineKeyboardButton( - text="🔄 Восстановить пробник", callback_data=f"restore_trial_{tg_id}" - ) - ) - builder.row( - InlineKeyboardButton( - text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}" - ) - ) - builder.row( - InlineKeyboardButton( - text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}" - ) - ) - builder.row( - InlineKeyboardButton( - text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}" - ) - ) + builder.row(InlineKeyboardButton(text="📝 Изменить баланс", callback_data=f"change_balance_{tg_id}")) + builder.row(InlineKeyboardButton(text="🔄 Восстановить пробник", callback_data=f"restore_trial_{tg_id}")) + builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}")) + builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}")) + builder.row(InlineKeyboardButton(text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}")) builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) user_info = ( @@ -702,19 +579,13 @@ async def handle_user_info( @router.callback_query(F.data.startswith("confirm_delete_user_"), IsAdminFilter()) -async def confirm_delete_user( - callback_query: types.CallbackQuery, state: FSMContext, session: Any -): +async def confirm_delete_user(callback_query: types.CallbackQuery, state: FSMContext, session: Any): tg_id = int(callback_query.data.split("_")[3]) confirmation_markup = InlineKeyboardMarkup( row_width=2, inline_keyboard=[ - [ - InlineKeyboardButton( - text="✅ Подтвердить", callback_data=f"delete_user_{tg_id}" - ) - ], + [InlineKeyboardButton(text="✅ Подтвердить", callback_data=f"delete_user_{tg_id}")], [InlineKeyboardButton(text="❌ Отменить", callback_data="user_editor")], ], ) @@ -729,9 +600,7 @@ async def confirm_delete_user( async def delete_user(callback_query: types.CallbackQuery, session: Any): tg_id = int(callback_query.data.split("_")[2]) - key_records = await session.fetch( - "SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id - ) + key_records = await session.fetch("SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id) async def delete_keys_from_servers(): try: @@ -742,9 +611,7 @@ async def delete_user(callback_query: types.CallbackQuery, session: Any): tasks.append(delete_key_from_cluster(cluster_id, email, client_id)) await asyncio.gather(*tasks) except Exception as e: - logger.error( - f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}" - ) + logger.error(f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}") await delete_keys_from_servers() @@ -754,13 +621,9 @@ async def delete_user(callback_query: types.CallbackQuery, session: Any): back_button = InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor") keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]]) - await callback_query.message.answer( - f"🗑️ Пользователь с ID {tg_id} был удален.", reply_markup=keyboard - ) + await callback_query.message.answer(f"🗑️ Пользователь с ID {tg_id} был удален.", reply_markup=keyboard) except Exception as e: - logger.error( - f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}" - ) + logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}") await callback_query.message.answer( f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова." ) diff --git a/handlers/buttons/yookassa.py b/handlers/buttons/yookassa.py index 52401db4..5bb26cbb 100644 --- a/handlers/buttons/yookassa.py +++ b/handlers/buttons/yookassa.py @@ -3,6 +3,10 @@ PAY_2 = "Оплатить" BACK = "⬅️ Назад" CUSTOM_SUM = "💰 Ввести свою сумму" PROFILE = "👤 Личный кабинет" -KEY_CREATION_PAYMENT_MESSAGE = "Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:" -KEY_RENEWAL_PAYMENT_MESSAGE = "Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:" +KEY_CREATION_PAYMENT_MESSAGE = ( + "Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:" +) +KEY_RENEWAL_PAYMENT_MESSAGE = ( + "Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:" +) DEFAULT_PAYMENT_MESSAGE = "Вы выбрали пополнение на {amount} рублей. Перейдите по ссылке для оплаты:" diff --git a/handlers/captcha.py b/handlers/captcha.py index c7d5ed16..fd67d534 100644 --- a/handlers/captcha.py +++ b/handlers/captcha.py @@ -3,7 +3,7 @@ from typing import Any from aiogram import F, Router from aiogram.fsm.context import FSMContext -from aiogram.types import CallbackQuery +from aiogram.types import CallbackQuery, Message from aiogram.utils.keyboard import InlineKeyboardBuilder from config import CAPTCHA_EMOJIS @@ -13,7 +13,7 @@ from logger import logger router = Router() -async def generate_captcha(state: FSMContext): +async def generate_captcha(message: Message, state: FSMContext): """Генерирует новую капчу и сохраняет правильный ответ в состоянии Пример словаря CAPTCHA_EMOJIS: @@ -28,48 +28,42 @@ async def generate_captcha(state: FSMContext): """ # Выбираем случайный эмодзи и его описание из конфига correct_emoji, correct_text = random.choice(list(CAPTCHA_EMOJIS.items())) - + # Получаем 3 случайных неправильных эмодзи - wrong_emojis = random.sample( - [e for e in CAPTCHA_EMOJIS.keys() if e != correct_emoji], 3 - ) - + wrong_emojis = random.sample([e for e in CAPTCHA_EMOJIS.keys() if e != correct_emoji], 3) + # Создаем список всех эмодзи и перемешиваем их all_emojis = [correct_emoji] + wrong_emojis random.shuffle(all_emojis) - + # Сохраняем правильный ответ в состоянии await state.update_data(correct_emoji=correct_emoji) - + await state.update_data(message=message) + # Создаем клавиатуру builder = InlineKeyboardBuilder() for emoji in all_emojis: builder.button(text=emoji, callback_data=f"captcha_{emoji}") builder.adjust(2, 2) - + return { "text": f"🔒 Для подтверждения что вы не робот,\nвыберите кнопку с {correct_text}", - "markup": builder.as_markup() + "markup": builder.as_markup(), } @router.callback_query(F.data.startswith("captcha_")) -async def check_captcha( - callback: CallbackQuery, state: FSMContext, session: Any, admin: bool -): +async def check_captcha(callback: CallbackQuery, state: FSMContext, session: Any, admin: bool): """Проверяет ответ пользователя на капчу""" selected_emoji = callback.data.split("captcha_")[1] state_data = await state.get_data() correct_emoji = state_data.get("correct_emoji") + message = state_data.get("message", callback.message) if selected_emoji == correct_emoji: logger.info(f"Пользователь {callback.message.chat.id} успешно прошел капчу") - await start_command(callback.message, state, session, admin) + await start_command(message, state, session, admin, False) else: - logger.warning( - f"Пользователь {callback.message.chat.id} неверно ответил на капчу" - ) - captcha = await generate_captcha(state) - await callback.message.answer( - text=captcha["text"], reply_markup=captcha["markup"] - ) + logger.warning(f"Пользователь {callback.message.chat.id} неверно ответил на капчу") + captcha = await generate_captcha(message, state) + await callback.message.answer(text=captcha["text"], reply_markup=captcha["markup"]) diff --git a/handlers/coupons.py b/handlers/coupons.py index eb0f7d4c..d010a028 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -18,9 +18,7 @@ router = Router() @router.callback_query(F.data == "activate_coupon") -async def handle_activate_coupon( - callback_query: types.CallbackQuery, state: FSMContext -): +async def handle_activate_coupon(callback_query: types.CallbackQuery, state: FSMContext): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) @@ -40,9 +38,7 @@ async def process_coupon_code(message: types.Message, state: FSMContext, session builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - await message.answer( - activation_result, reply_markup=builder.as_markup() - ) + await message.answer(activation_result, reply_markup=builder.as_markup()) await state.clear() diff --git a/handlers/donate.py b/handlers/donate.py index ad460085..3de7d323 100644 --- a/handlers/donate.py +++ b/handlers/donate.py @@ -22,11 +22,7 @@ async def process_donate(callback_query: types.CallbackQuery, state: FSMContext) await state.clear() builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot" - ) - ) + builder.row(InlineKeyboardButton(text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot")) builder.row( InlineKeyboardButton( text="💰 Ввести сумму доната", @@ -44,14 +40,10 @@ async def process_donate(callback_query: types.CallbackQuery, state: FSMContext) @router.callback_query(F.data == "enter_custom_donate_amount") -async def process_enter_donate_amount( - callback_query: types.CallbackQuery, state: FSMContext -): +async def process_enter_donate_amount(callback_query: types.CallbackQuery, state: FSMContext): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="donate")) - await callback_query.message.answer( - "💸 Введите сумму доната в рублях:", reply_markup=builder.as_markup() - ) + await callback_query.message.answer("💸 Введите сумму доната в рублях:", reply_markup=builder.as_markup()) await state.set_state(DonateState.entering_donate_amount) @@ -60,9 +52,7 @@ async def process_donate_amount_input(message: types.Message, state: FSMContext) if message.text.isdigit(): amount = int(message.text) if amount // RUB_TO_XTR <= 0: - await message.answer( - f"Сумма доната должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:" - ) + await message.answer(f"Сумма доната должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:") return await state.update_data(amount=amount) @@ -99,9 +89,7 @@ async def on_successful_donate(message: types.Message, state: FSMContext): try: amount = float(message.successful_payment.invoice_payload.split("_")[0]) builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") - ) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await message.answer( text=f"🙏 Спасибо за донат {amount} рублей! Ваша поддержка очень важна для нас. 💖", reply_markup=builder.as_markup(), diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index a77e63ba..3b278264 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -29,9 +29,7 @@ async def send_instructions( if not os.path.isfile(image_path): if isinstance(callback_query_or_message, types.CallbackQuery): - await callback_query_or_message.message.answer( - "Файл изображения не найден." - ) + await callback_query_or_message.message.answer("Файл изображения не найден.") else: await callback_query_or_message.answer("Файл изображения не найден.") return @@ -71,9 +69,7 @@ async def process_connect_pc(callback_query: types.CallbackQuery, session: Any): ) if not record: - await callback_query.message.answer( - "❌ Ключ не найден. Проверьте имя ключа. 🔍" - ) + await callback_query.message.answer("❌ Ключ не найден. Проверьте имя ключа. 🔍") return key = record["key"] @@ -81,14 +77,8 @@ async def process_connect_pc(callback_query: types.CallbackQuery, session: Any): instruction_message = f"{key_message}{INSTRUCTION_PC}" builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="💻 Подключить Windows", url=f"{CONNECT_WINDOWS}{key}" - ) - ) - builder.row( - InlineKeyboardButton(text="💻 Подключить MacOS", url=f"{CONNECT_MACOS}{key}") - ) + builder.row(InlineKeyboardButton(text="💻 Подключить Windows", url=f"{CONNECT_WINDOWS}{key}")) + builder.row(InlineKeyboardButton(text="💻 Подключить MacOS", url=f"{CONNECT_MACOS}{key}")) builder.row(InlineKeyboardButton(text="🆘 Поддержка", url=f"{SUPPORT_CHAT_URL}")) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) @@ -104,11 +94,7 @@ async def process_connect_tv(callback_query: types.CallbackQuery): key_name = callback_query.data.split("|")[1] builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="▶ Продолжить", callback_data=f"continue_tv|{key_name}" - ) - ) + builder.row(InlineKeyboardButton(text="▶ Продолжить", callback_data=f"continue_tv|{key_name}")) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await callback_query.message.answer( @@ -146,13 +132,7 @@ async def process_continue_tv(callback_query: types.CallbackQuery): message_text = SUBSCRIPTION_DETAILS_TEXT.format(subscription_link=subscription_link) builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="📖 Полная инструкция", url="https://vpn4tv.com/quick-guide.html" - ) - ) + builder.row(InlineKeyboardButton(text="📖 Полная инструкция", url="https://vpn4tv.com/quick-guide.html")) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - await callback_query.message.answer( - text=message_text, reply_markup=builder.as_markup() - ) + await callback_query.message.answer(text=message_text, reply_markup=builder.as_markup()) diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 625947b5..bcb90715 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -55,16 +55,12 @@ class Form(StatesGroup): @router.callback_query(F.data == "create_key") -async def confirm_create_new_key( - callback_query: CallbackQuery, state: FSMContext, session: Any -): +async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContext, session: Any): tg_id = callback_query.message.chat.id logger.info(f"User {tg_id} confirmed creation of a new key.") - logger.info( - f"Balance for user {tg_id} is sufficient. Proceeding with key creation." - ) + logger.info(f"Balance for user {tg_id} is sufficient. Proceeding with key creation.") await handle_key_creation(tg_id, state, session, callback_query) @@ -83,9 +79,7 @@ async def handle_key_creation( expiry_time = current_time + timedelta(days=TRIAL_TIME) logger.info(f"Assigned 1-day trial to user {tg_id}.") - await session.execute( - "UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id - ) + await session.execute("UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id) await create_key(tg_id, expiry_time, state, session, message_or_query) else: builder = InlineKeyboardBuilder() @@ -107,9 +101,7 @@ async def handle_key_creation( ) ) - builder.row( - InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") - ) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await message_or_query.message.answer( "💳 Выберите тарифный план для создания нового ключа:", @@ -153,12 +145,8 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any): await handle_custom_amount_input(callback_query, session) else: builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay") - ) - builder.row( - InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") - ) + builder.row(InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await callback_query.message.answer( f"💳 Недостаточно средств. Для продолжения необходимо пополнить баланс на {required_amount}₽.", @@ -193,9 +181,7 @@ async def create_key( ) if not existing_key: break - logger.warning( - f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one." - ) + logger.warning(f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one.") client_id = str(uuid.uuid4()) email = key_name.lower() diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index 47f9401c..f0d149d4 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -33,9 +33,7 @@ async def create_key_on_cluster(cluster_id, tg_id, client_id, email, expiry_time server_name = server_info.get("server_name", "unknown") if not inbound_id: - logger.warning( - f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск." - ) + logger.warning(f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск.") return if SUPERNODE: @@ -56,7 +54,7 @@ async def create_key_on_cluster(cluster_id, tg_id, client_id, email, expiry_time enable=True, flow="xtls-rprx-vision", inbound_id=int(inbound_id), - sub_id=sub_id + sub_id=sub_id, ) if SUPERNODE: @@ -73,8 +71,6 @@ async def create_key_on_cluster(cluster_id, tg_id, client_id, email, expiry_time raise e - - async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, total_gb): try: servers = await get_servers_from_db() @@ -95,9 +91,7 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to server_name = server_info.get("server_name", "unknown") if not inbound_id: - logger.warning( - f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск." - ) + logger.warning(f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск.") continue if SUPERNODE: @@ -108,23 +102,13 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to sub_id = unique_email tasks.append( - extend_client_key( - xui, - int(inbound_id), - unique_email, - new_expiry_time, - client_id, - total_gb, - sub_id - ) + extend_client_key(xui, int(inbound_id), unique_email, new_expiry_time, client_id, total_gb, sub_id) ) await asyncio.gather(*tasks) except Exception as e: - logger.error( - f"Не удалось продлить ключ {client_id} в кластере {cluster_id}: {e}" - ) + logger.error(f"Не удалось продлить ключ {client_id} в кластере {cluster_id}: {e}") raise e @@ -171,9 +155,7 @@ async def delete_key_from_cluster(cluster_id, email, client_id): await asyncio.gather(*tasks) except Exception as e: - logger.error( - f"Не удалось удалить ключ {client_id} в кластере {cluster_id}: {e}" - ) + logger.error(f"Не удалось удалить ключ {client_id} в кластере {cluster_id}: {e}") raise e @@ -212,18 +194,14 @@ async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id enable=True, flow="xtls-rprx-vision", inbound_id=int(inbound_id), - sub_id=email + sub_id=email, ) ) await asyncio.gather(*tasks) - logger.info( - f"Ключ успешно обновлен для {client_id} на всех серверах в кластере {cluster_id}" - ) + logger.info(f"Ключ успешно обновлен для {client_id} на всех серверах в кластере {cluster_id}") except Exception as e: - logger.error( - f"Ошибка при обновлении ключа на серверах кластера {cluster_id} для {client_id}: {e}" - ) + logger.error(f"Ошибка при обновлении ключа на серверах кластера {cluster_id} для {client_id}: {e}") raise e diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 8a9cbbb7..5fdc6f16 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -86,9 +86,7 @@ async def process_callback_or_message_view_keys( inline_keyboard, response_message = build_keys_response(records) image_path = os.path.join("img", "pic_keys.jpg") - await send_with_optional_image( - send_message, send_photo, image_path, response_message, inline_keyboard - ) + await send_with_optional_image(send_message, send_photo, image_path, response_message, inline_keyboard) except Exception as e: error_message = f"Ошибка при получении ключей: {e}" await send_message(text=error_message) @@ -105,15 +103,10 @@ def build_keys_response(records): key_name = record["email"] expiry_date = datetime.utcfromtimestamp(record["expiry_time"] / 1000).strftime("%d.%m.%Y") builder.row( - InlineKeyboardButton( - text=f"🔑 {key_name} (до {expiry_date})", - callback_data=f"view_key|{key_name}" - ) + InlineKeyboardButton(text=f"🔑 {key_name} (до {expiry_date})", callback_data=f"view_key|{key_name}") ) - builder.row( - InlineKeyboardButton(text="➕ Добавить подписку", callback_data="create_key") - ) + builder.row(InlineKeyboardButton(text="➕ Добавить подписку", callback_data="create_key")) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) @@ -125,18 +118,14 @@ def build_keys_response(records): return inline_keyboard, response_message -async def send_with_optional_image( - send_message, send_photo, image_path, text, keyboard -): +async def send_with_optional_image(send_message, send_photo, image_path, text, keyboard): """ Отправляет сообщение с изображением, если файл существует. В противном случае отправляет только текст. """ if os.path.isfile(image_path): with open(image_path, "rb") as image_file: await send_photo( - photo=BufferedInputFile( - image_file.read(), filename=os.path.basename(image_path) - ), + photo=BufferedInputFile(image_file.read(), filename=os.path.basename(image_path)), caption=text, reply_markup=keyboard, ) @@ -184,9 +173,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session ) formatted_expiry_date = expiry_date.strftime("%d %B %Y года") - response_message = key_message( - key, formatted_expiry_date, days_left_message, server_name - ) + response_message = key_message(key, formatted_expiry_date, days_left_message, server_name) builder = InlineKeyboardBuilder() @@ -200,51 +187,29 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session builder.row( InlineKeyboardButton(text=DOWNLOAD_IOS_BUTTON, url=DOWNLOAD_IOS), - InlineKeyboardButton( - text=DOWNLOAD_ANDROID_BUTTON, url=DOWNLOAD_ANDROID - ), + InlineKeyboardButton(text=DOWNLOAD_ANDROID_BUTTON, url=DOWNLOAD_ANDROID), ) builder.row( InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{key}"), - InlineKeyboardButton( - text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{key}" - ), + InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{key}"), ) builder.row( - InlineKeyboardButton( - text=PC_BUTTON, callback_data=f"connect_pc|{key_name}" - ), - InlineKeyboardButton( - text=TV_BUTTON, callback_data=f"connect_tv|{key_name}" - ), + InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{key_name}"), + InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{key_name}"), ) # ✅ Добавлена проверка флага ENABLE_DELETE_KEY_BUTTON if ENABLE_DELETE_KEY_BUTTON: builder.row( - InlineKeyboardButton( - text="⏳ Продлить", callback_data=f"renew_key|{key_name}" - ), - InlineKeyboardButton( - text="❌ Удалить", callback_data=f"delete_key|{key_name}" - ), + InlineKeyboardButton(text="⏳ Продлить", callback_data=f"renew_key|{key_name}"), + InlineKeyboardButton(text="❌ Удалить", callback_data=f"delete_key|{key_name}"), ) else: - builder.row( - InlineKeyboardButton( - text="⏳ Продлить", callback_data=f"renew_key|{key_name}" - ) - ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", callback_data="view_keys" - ) - ) - builder.row( - InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") - ) + builder.row(InlineKeyboardButton(text="⏳ Продлить", callback_data=f"renew_key|{key_name}")) + builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="view_keys")) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) keyboard = builder.as_markup() @@ -273,9 +238,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session @router.callback_query(F.data.startswith("update_subscription|")) -async def process_callback_update_subscription( - callback_query: types.CallbackQuery, session: Any -): +async def process_callback_update_subscription(callback_query: types.CallbackQuery, session: Any): tg_id = callback_query.message.chat.id email = callback_query.data.split("|")[1] try: @@ -335,9 +298,7 @@ async def process_callback_update_subscription( else: await callback_query.message.answer("Ключ не найден в базе данных.") except Exception as e: - await handle_error( - tg_id, callback_query, f"Ошибка при обновлении подписки: {e}" - ) + await handle_error(tg_id, callback_query, f"Ошибка при обновлении подписки: {e}") @router.callback_query(F.data.startswith("delete_key|")) @@ -352,11 +313,7 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery): callback_data=f"confirm_delete|{client_id}", ) ], - [ - types.InlineKeyboardButton( - text="❌ Нет, отменить", callback_data="view_keys" - ) - ], + [types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="view_keys")], ] ) @@ -391,43 +348,39 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio builder.row( InlineKeyboardButton( - text=f'📅 1 месяц ({RENEWAL_PLANS["1"]["price"]} руб.)', + text=f"📅 1 месяц ({RENEWAL_PLANS['1']['price']} руб.)", callback_data=f"renew_plan|1|{client_id}", ) ) builder.row( InlineKeyboardButton( - text=f'📅 3 месяца ({RENEWAL_PLANS["3"]["price"]} руб.) {DISCOUNTS["3"]}% скидка', + text=f"📅 3 месяца ({RENEWAL_PLANS['3']['price']} руб.) {DISCOUNTS['3']}% скидка", callback_data=f"renew_plan|3|{client_id}", ) ) builder.row( InlineKeyboardButton( - text=f'📅 6 месяцев ({RENEWAL_PLANS["6"]["price"]} руб.) {DISCOUNTS["6"]}% скидка', + text=f"📅 6 месяцев ({RENEWAL_PLANS['6']['price']} руб.) {DISCOUNTS['6']}% скидка", callback_data=f"renew_plan|6|{client_id}", ) ) builder.row( InlineKeyboardButton( - text=f'📅 12 месяцев ({RENEWAL_PLANS["12"]["price"]} руб.) ({DISCOUNTS["12"]}% 🔥)', + text=f"📅 12 месяцев ({RENEWAL_PLANS['12']['price']} руб.) ({DISCOUNTS['12']}% 🔥)", callback_data=f"renew_plan|12|{client_id}", ) ) - back_button = InlineKeyboardButton( - text="🔙 Назад", callback_data="view_keys" - ) + back_button = InlineKeyboardButton(text="🔙 Назад", callback_data="view_keys") builder.row(back_button) balance = await get_balance(tg_id) response_message = PLAN_SELECTION_MSG.format( balance=balance, - expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime( - "%Y-%m-%d %H:%M:%S" - ), + expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S"), ) await callback_query.message.answer( @@ -441,21 +394,15 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio @router.callback_query(F.data.startswith("confirm_delete|")) -async def process_callback_confirm_delete( - callback_query: types.CallbackQuery, session: Any -): +async def process_callback_confirm_delete(callback_query: types.CallbackQuery, session: Any): email = callback_query.data.split("|")[1] try: - record = await session.fetchrow( - "SELECT client_id FROM keys WHERE email = $1", email - ) + record = await session.fetchrow("SELECT client_id FROM keys WHERE email = $1", email) if record: client_id = record["client_id"] response_message = "Ключ успешно удален." - back_button = types.InlineKeyboardButton( - text="Назад", callback_data="view_keys" - ) + back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_keys") keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) await delete_key(client_id) @@ -470,9 +417,7 @@ async def process_callback_confirm_delete( try: tasks = [] for cluster_id, cluster in servers.items(): - tasks.append( - delete_key_from_cluster(cluster_id, email, client_id) - ) + tasks.append(delete_key_from_cluster(cluster_id, email, client_id)) await asyncio.gather(*tasks) @@ -485,9 +430,7 @@ async def process_callback_confirm_delete( else: response_message = "Ключ не найден или уже удален." - back_button = types.InlineKeyboardButton( - text="Назад", callback_data="view_keys" - ) + back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_keys") keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) await callback_query.message.answer( @@ -529,7 +472,9 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery, sessi if balance < cost: required_amount = cost - balance - logger.info(f"[RENEW] Пользователю {tg_id} не хватает {required_amount}₽. Запуск доплаты через {USE_NEW_PAYMENT_FLOW}") + logger.info( + f"[RENEW] Пользователю {tg_id} не хватает {required_amount}₽. Запуск доплаты через {USE_NEW_PAYMENT_FLOW}" + ) await save_temporary_data( session, @@ -610,4 +555,3 @@ async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_g logger.info(f"[RENEW] Ключ {client_id} успешно продлён на {plan} мес. для пользователя {tg_id}.") await renew_key_on_servers() - diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index 35b19f29..4f21ec14 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -13,6 +13,7 @@ from logger import logger # Глобальная переменная для пула соединений db_pool = None + async def init_db_pool(): """ Инициализация пула соединений, если он ещё не создан. @@ -21,6 +22,7 @@ async def init_db_pool(): if not db_pool: db_pool = await asyncpg.create_pool(dsn=DATABASE_URL, min_size=5, max_size=20) + async def fetch_url_content(url, tg_id): try: logger.info(f"Получение URL: {url} для tg_id: {tg_id}") @@ -32,9 +34,7 @@ async def fetch_url_content(url, tg_id): logger.info(f"Успешно получен контент с {url} для tg_id: {tg_id}") return base64.b64decode(content).decode("utf-8").split("\n") else: - logger.error( - f"Не удалось получить {url} для tg_id: {tg_id}, статус: {response.status}" - ) + logger.error(f"Не удалось получить {url} для tg_id: {tg_id}, статус: {response.status}") return [] except asyncio.TimeoutError: logger.error(f"Таймаут при получении {url} для tg_id: {tg_id}") @@ -43,6 +43,7 @@ async def fetch_url_content(url, tg_id): logger.error(f"Ошибка при получении {url} для tg_id: {tg_id}: {e}") return [] + async def combine_unique_lines(urls, tg_id, query_string): if SUPERNODE: logger.info(f"Режим SUPERNODE активен. Возвращаем первую ссылку для tg_id: {tg_id}") @@ -51,9 +52,7 @@ async def combine_unique_lines(urls, tg_id, query_string): url_with_query = f"{urls[0]}?{query_string}" if query_string else urls[0] return await fetch_url_content(url_with_query, tg_id) - logger.info( - f"Начинаем объединение подписок для tg_id: {tg_id}, запрос: {query_string}" - ) + logger.info(f"Начинаем объединение подписок для tg_id: {tg_id}, запрос: {query_string}") urls_with_query = [f"{url}?{query_string}" if query_string else url for url in urls] logger.info(f"Составлены URL-адреса: {urls_with_query}") @@ -65,19 +64,17 @@ async def combine_unique_lines(urls, tg_id, query_string): for lines in results: all_lines.update(filter(None, lines)) - logger.info( - f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов для tg_id: {tg_id}" - ) + logger.info(f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов для tg_id: {tg_id}") return list(all_lines) + transition_date = datetime.strptime(TRANSITION_DATE_STR, "%Y-%m-%d %H:%M:%S") transition_timestamp_ms = int(transition_date.timestamp() * 1000) transition_timestamp_ms_adjusted = transition_timestamp_ms - (3 * 60 * 60 * 1000) -logger.info( - f"Время перехода (с поправкой на часовой пояс): {transition_timestamp_ms_adjusted}" -) +logger.info(f"Время перехода (с поправкой на часовой пояс): {transition_timestamp_ms_adjusted}") + async def handle_old_subscription(request): email = request.match_info.get("email") @@ -95,9 +92,7 @@ async def handle_old_subscription(request): await init_db_pool() async with db_pool.acquire() as conn: - key_info = await conn.fetchrow( - "SELECT created_at, server_id FROM keys WHERE email = $1", email - ) + key_info = await conn.fetchrow("SELECT created_at, server_id FROM keys WHERE email = $1", email) if not key_info: logger.warning(f"Клиент с email {email} не найден в базе.") @@ -115,14 +110,10 @@ async def handle_old_subscription(request): status=400, ) - logger.info( - f"Значение created_at для клиента с email {email}: {created_at_ms}, кластер: {cluster_name}" - ) + logger.info(f"Значение created_at для клиента с email {email}: {created_at_ms}, кластер: {cluster_name}") created_at_datetime = datetime.utcfromtimestamp(created_at_ms / 1000) - logger.info( - f"Время создания клиента в формате datetime (UTC): {created_at_datetime}" - ) + logger.info(f"Время создания клиента в формате datetime (UTC): {created_at_datetime}") if created_at_ms >= transition_timestamp_ms_adjusted: logger.info(f"Клиент с email {email} является новым.") @@ -135,23 +126,18 @@ async def handle_old_subscription(request): cluster_servers = servers.get(cluster_name, []) logger.info(f"Сервера в кластере: {cluster_servers}") - urls = [ - f"{server['subscription_url']}/{email}" for server in cluster_servers - ] + urls = [f"{server['subscription_url']}/{email}" for server in cluster_servers] combined_subscriptions = await combine_unique_lines(urls, email, "") - base64_encoded = base64.b64encode( - "\n".join(combined_subscriptions).encode("utf-8") - ).decode("utf-8") + base64_encoded = base64.b64encode("\n".join(combined_subscriptions).encode("utf-8")).decode("utf-8") encoded_project_name = f"{PROJECT_NAME} - {SUB_MESSAGE}" headers = { "Content-Type": "text/plain; charset=utf-8", "Content-Disposition": "inline", "profile-update-interval": "7", - "profile-title": "base64:" - + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"), + "profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"), } logger.info(f"Возвращаем объединенные подписки для email: {email}") @@ -175,9 +161,7 @@ async def handle_new_subscription(request): await init_db_pool() async with db_pool.acquire() as conn: - client_data = await conn.fetchrow( - "SELECT tg_id, server_id FROM keys WHERE email = $1", email - ) + client_data = await conn.fetchrow("SELECT tg_id, server_id FROM keys WHERE email = $1", email) if not client_data: logger.warning(f"Клиент с email {email} не найден в базе.") @@ -199,18 +183,14 @@ async def handle_new_subscription(request): servers = await get_servers_from_db() cluster_servers = servers.get(cluster_name, []) - urls = [ - f"{server['subscription_url']}/{email}" for server in cluster_servers - ] + urls = [f"{server['subscription_url']}/{email}" for server in cluster_servers] query_string = request.query_string logger.info(f"Извлечен query string: {query_string}") combined_subscriptions = await combine_unique_lines(urls, tg_id, query_string) - base64_encoded = base64.b64encode( - "\n".join(combined_subscriptions).encode("utf-8") - ).decode("utf-8") + base64_encoded = base64.b64encode("\n".join(combined_subscriptions).encode("utf-8")).decode("utf-8") encoded_project_name = f"{PROJECT_NAME} - {SUB_MESSAGE}" @@ -218,8 +198,7 @@ async def handle_new_subscription(request): "Content-Type": "text/plain; charset=utf-8", "Content-Disposition": "inline", "profile-update-interval": "7", - "profile-title": "base64:" - + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"), + "profile-title": "base64:" + base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"), } logger.info(f"Возвращаем объединенные подписки для email: {email}") diff --git a/handlers/keys/trial_key.py b/handlers/keys/trial_key.py index 97821fb0..9ec15090 100644 --- a/handlers/keys/trial_key.py +++ b/handlers/keys/trial_key.py @@ -16,9 +16,7 @@ from logger import logger async def create_trial_key(tg_id: int, session: Any): try: - trial_status = await session.fetchval( - "SELECT trial FROM connections WHERE tg_id = $1", tg_id - ) + trial_status = await session.fetchval("SELECT trial FROM connections WHERE tg_id = $1", tg_id) if trial_status == 1: return {"error": "Вы уже использовали пробную версию."} except Exception as e: @@ -67,7 +65,7 @@ async def create_trial_key(tg_id: int, session: Any): enable=True, flow="xtls-rprx-vision", inbound_id=int(server_info["inbound_id"]), - sub_id=base_email + sub_id=base_email, ) ) diff --git a/handlers/notifications.py b/handlers/notifications.py index cdc2263c..7ac9210c 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -36,6 +36,7 @@ from logger import logger router = Router() + async def periodic_expired_keys_check(bot: Bot): """Периодическая проверка истекших ключей с кастомным интервалом.""" while True: @@ -54,7 +55,6 @@ async def periodic_expired_keys_check(bot: Bot): await asyncio.sleep(EXPIRED_KEYS_CHECK_INTERVAL) - async def notify_expiring_keys(bot: Bot): conn = None try: @@ -84,21 +84,16 @@ async def notify_expiring_keys(bot: Bot): logger.info("Соединение с базой данных закрыто.") - async def is_bot_blocked(bot: Bot, chat_id: int) -> bool: if DEV_MODE: return False try: member = await bot.get_chat_member(chat_id, bot.id) blocked = member.status == "left" - logger.info( - f"Статус бота для пользователя {chat_id}: {'заблокирован' if blocked else 'активен'}" - ) + logger.info(f"Статус бота для пользователя {chat_id}: {'заблокирован' if blocked else 'активен'}") return blocked except Exception as e: - logger.warning( - f"Не удалось проверить статус бота для пользователя {chat_id}: {e}" - ) + logger.warning(f"Не удалось проверить статус бота для пользователя {chat_id}: {e}") return False @@ -137,7 +132,11 @@ async def process_10h_record(record, bot, conn): time_left = expiry_date - current_date days_left_message = ( - "Ключ истек" if time_left.total_seconds() <= 0 else f"{time_left.days}" if time_left.days > 0 else f"{time_left.seconds // 3600}" + "Ключ истек" + if time_left.total_seconds() <= 0 + else f"{time_left.days}" + if time_left.days > 0 + else f"{time_left.seconds // 3600}" ) message = KEY_EXPIRY_10H.format( @@ -200,7 +199,6 @@ async def notify_24h_keys( logger.info("Обработка всех уведомлений за 24 часа завершена.") - async def process_24h_record(record, bot, conn): tg_id = record["tg_id"] email = record["email"] @@ -213,7 +211,11 @@ async def process_24h_record(record, bot, conn): time_left = expiry_date - current_date days_left_message = ( - "Ключ истек" if time_left.total_seconds() <= 0 else f"{time_left.days}" if time_left.days > 0 else f"{time_left.seconds // 3600}" + "Ключ истек" + if time_left.total_seconds() <= 0 + else f"{time_left.days}" + if time_left.days > 0 + else f"{time_left.seconds // 3600}" ) message_24h = KEY_EXPIRY_24H.format( @@ -287,9 +289,7 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): username = user.get("username", "Пользователь") try: - can_notify = await check_notification_time( - tg_id, "inactive_trial", hours=24, session=conn - ) + can_notify = await check_notification_time(tg_id, "inactive_trial", hours=24, session=conn) if can_notify: builder = InlineKeyboardBuilder() @@ -299,11 +299,7 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): callback_data="create_key", ) ) - builder.row( - types.InlineKeyboardButton( - text="👤 Личный кабинет", callback_data="profile" - ) - ) + builder.row(types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) keyboard = builder.as_markup() message = ( @@ -315,20 +311,14 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): try: await bot.send_message(tg_id, message, reply_markup=keyboard) - logger.info( - f"Отправлено уведомление неактивному пользователю {tg_id}." - ) + logger.info(f"Отправлено уведомление неактивному пользователю {tg_id}.") await add_notification(tg_id, "inactive_trial", session=conn) except TelegramForbiddenError: - logger.warning( - f"Бот заблокирован пользователем {tg_id}. Добавляем в blocked_users." - ) + logger.warning(f"Бот заблокирован пользователем {tg_id}. Добавляем в blocked_users.") await add_blocked_user(tg_id, conn) except Exception as e: - logger.error( - f"Ошибка при отправке уведомления пользователю {tg_id}: {e}" - ) + logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}") except Exception as e: logger.error(f"Ошибка при обработке пользователя {tg_id}: {e}") @@ -378,13 +368,7 @@ async def process_key(record, bot, conn): ) keyboard = types.InlineKeyboardMarkup( - inline_keyboard=[ - [ - types.InlineKeyboardButton( - text="👤 Личный кабинет", callback_data="profile" - ) - ] - ] + inline_keyboard=[[types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")]] ) try: @@ -446,15 +430,12 @@ async def process_key(record, bot, conn): logger.error(f"Ошибка при обработке ключа для клиента {tg_id}: {e}") - async def check_online_users(): servers = await get_servers_from_db() for cluster_id, cluster in servers.items(): for server_id, server in enumerate(cluster): - xui = AsyncApi( - server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD - ) + xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD) await xui.login() try: online_users = len(await xui.client.online()) @@ -462,6 +443,4 @@ async def check_online_users(): f"Сервер '{server['server_name']}' доступен, текущее количество активных пользователей: {online_users}." ) except Exception as e: - logger.error( - f"Не удалось проверить пользователей на сервере {server_id}: {e}" - ) + logger.error(f"Не удалось проверить пользователей на сервере {server_id}: {e}") diff --git a/handlers/pay.py b/handlers/pay.py index 2b7c753c..24dd97a1 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -54,15 +54,9 @@ async def handle_pay(callback_query: CallbackQuery): ) ) - builder.row( - InlineKeyboardButton( - text="🎟️ Активировать купон", callback_data="activate_coupon" - ) - ) + builder.row(InlineKeyboardButton(text="🎟️ Активировать купон", callback_data="activate_coupon")) if DONATIONS_ENABLE: - builder.row( - InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate") - ) + builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index 37aa3692..4a796631 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -52,9 +52,7 @@ if ROBOKASSA_ENABLE: def generate_payment_link(amount, inv_id, description, tg_id): """Генерация ссылки на оплату.""" - logger.debug( - f"Generating payment link for amount: {amount}, inv_id: {inv_id}, description: {description}" - ) + logger.debug(f"Generating payment link for amount: {amount}, inv_id: {inv_id}, description: {description}") payment_link = robokassa._payment.link.generate_by_script( out_sum=amount, inv_id=inv_id, @@ -66,9 +64,7 @@ def generate_payment_link(amount, inv_id, description, tg_id): @router.callback_query(F.data == "pay_robokassa") -async def process_callback_pay_robokassa( - callback_query: types.CallbackQuery, state: FSMContext, session: Any -): +async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, state: FSMContext, session: Any): tg_id = callback_query.message.chat.id logger.info(f"User {tg_id} initiated Robokassa payment.") @@ -78,18 +74,18 @@ async def process_callback_pay_robokassa( builder.row( InlineKeyboardButton( text=PAYMENT_OPTIONS[i]["text"], - callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}', + callback_data=f"robokassa_amount|{PAYMENT_OPTIONS[i]['callback_data']}", ), InlineKeyboardButton( text=PAYMENT_OPTIONS[i + 1]["text"], - callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i + 1]["callback_data"]}', + callback_data=f"robokassa_amount|{PAYMENT_OPTIONS[i + 1]['callback_data']}", ), ) else: builder.row( InlineKeyboardButton( text=PAYMENT_OPTIONS[i]["text"], - callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}', + callback_data=f"robokassa_amount|{PAYMENT_OPTIONS[i]['callback_data']}", ) ) builder.row( @@ -117,9 +113,7 @@ async def process_callback_pay_robokassa( @router.callback_query(F.data.startswith("robokassa_amount|")) -async def process_amount_selection( - callback_query: types.CallbackQuery, state: FSMContext -): +async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): logger.info(f"Получены данные callback_data: {callback_query.data}") data = callback_query.data.split("|") @@ -173,9 +167,7 @@ async def robokassa_webhook(request): shp_id = params.get("shp_id") signature_value = params.get("SignatureValue") - logger.info( - f"OutSum: {amount}, InvId: {inv_id}, shp_id: {shp_id}, SignatureValue: {signature_value}" - ) + logger.info(f"OutSum: {amount}, InvId: {inv_id}, shp_id: {shp_id}, SignatureValue: {signature_value}") if not check_payment_signature(params): logger.error("Неверная подпись или данные запроса.") @@ -214,9 +206,7 @@ def check_payment_signature(params): logger.info(f"Signature string before hashing: {signature_string}") - expected_signature = ( - hashlib.md5(signature_string.encode("utf-8")).hexdigest().upper() - ) + expected_signature = hashlib.md5(signature_string.encode("utf-8")).hexdigest().upper() logger.info(f"Expected signature: {expected_signature}") logger.info(f"Received signature: {signature_value}") @@ -225,9 +215,7 @@ def check_payment_signature(params): @router.callback_query(F.data == "enter_custom_amount_robokassa") -async def process_custom_amount_selection( - callback_query: types.CallbackQuery, state: FSMContext -): +async def process_custom_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): tg_id = callback_query.message.chat.id logger.info(f"User {tg_id} chose to enter a custom amount.") @@ -239,13 +227,13 @@ async def process_custom_amount_selection( reply_markup=builder.as_markup(), ) - await state.set_state( - ReplenishBalanceState.waiting_for_payment_confirmation_robokassa - ) + await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa) @router.message(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa) -async def handle_custom_amount_input(message: types.Message | types.CallbackQuery, state: FSMContext = None, session: Any = None): +async def handle_custom_amount_input( + message: types.Message | types.CallbackQuery, state: FSMContext = None, session: Any = None +): if isinstance(message, types.CallbackQuery): tg_id = message.message.chat.id else: @@ -255,7 +243,6 @@ async def handle_custom_amount_input(message: types.Message | types.CallbackQuer inv_id = 0 try: - conn = await asyncpg.connect(DATABASE_URL) user_data = await get_temporary_data(conn, tg_id) await conn.close() @@ -283,9 +270,13 @@ async def handle_custom_amount_input(message: types.Message | types.CallbackQuer ) if state_type == "waiting_for_payment": - message_text = f"Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:" + message_text = ( + f"Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:" + ) elif state_type == "waiting_for_renewal_payment": - message_text = f"Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:" + message_text = ( + f"Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:" + ) else: await message.answer("Некорректное состояние данных. Попробуйте снова.") return diff --git a/handlers/profile.py b/handlers/profile.py index dfd5fa23..185eb793 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -51,9 +51,7 @@ async def process_callback_view_profile( try: trial_status = await get_trial(chat_id, conn) - profile_message = profile_message_send( - username, chat_id, int(balance), key_count - ) + profile_message = profile_message_send(username, chat_id, int(balance), key_count) if key_count == 0: profile_message += "\n
🔧 Нажмите кнопку ➕ Подписка, чтобы настроить VPN-подключение
" @@ -81,9 +79,7 @@ async def process_callback_view_profile( InlineKeyboardButton(text=INSTRUCTIONS, callback_data="instructions"), ) if admin: - builder.row( - InlineKeyboardButton(text="🔧 Администратор", callback_data="admin") - ) + builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="start")) if os.path.isfile(image_path): @@ -122,10 +118,8 @@ async def balance_handler(callback_query: types.CallbackQuery): builder.row(InlineKeyboardButton(text=BALANCE_HISTORY, callback_data="balance_history")) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) - await callback_query.message.answer( - "💰 Управление балансом:", - reply_markup=builder.as_markup() - ) + await callback_query.message.answer("💰 Управление балансом:", reply_markup=builder.as_markup()) + @router.callback_query(F.data == "balance_history") async def balance_history_handler(callback_query: types.CallbackQuery, session: Any): @@ -145,10 +139,10 @@ async def balance_history_handler(callback_query: types.CallbackQuery, session: if records: history_text = "📊 Последние 3 операции с балансом:\n\n" for record in records: - amount = record['amount'] - payment_system = record['payment_system'] - status = record['status'] - date = record['created_at'].strftime('%Y-%m-%d %H:%M:%S') + amount = record["amount"] + payment_system = record["payment_system"] + status = record["status"] + date = record["created_at"].strftime("%Y-%m-%d %H:%M:%S") history_text += ( f"Сумма: {amount}₽\n" f"Способ оплаты: {payment_system}\n" @@ -158,11 +152,7 @@ async def balance_history_handler(callback_query: types.CallbackQuery, session: else: history_text = "❌ У вас пока нет операций с балансом." - await callback_query.message.answer( - history_text, - reply_markup=builder.as_markup() - ) - + await callback_query.message.answer(history_text, reply_markup=builder.as_markup()) @router.message(F.text == "/tariffs") diff --git a/handlers/start.py b/handlers/start.py index 070f57fa..d7560ac3 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -13,6 +13,7 @@ from aiogram.types import ( from aiogram.utils.keyboard import InlineKeyboardBuilder from config import ( + CAPTCHA_ENABLE, CHANNEL_EXISTS, CHANNEL_URL, CONNECT_ANDROID, @@ -21,7 +22,6 @@ from config import ( DOWNLOAD_ANDROID, DOWNLOAD_IOS, SUPPORT_CHAT_URL, - CAPTCHA_ENABLE, ) from database import ( add_connection, @@ -38,32 +38,32 @@ from handlers.buttons.add_subscribe import ( PC_BUTTON, TV_BUTTON, ) +from handlers.captcha import generate_captcha from handlers.keys.key_management import create_key from handlers.keys.trial_key import create_trial_key from handlers.texts import INSTRUCTIONS_TRIAL, WELCOME_TEXT, get_about_vpn from logger import logger -from handlers.captcha import generate_captcha router = Router() @router.callback_query(F.data == "start") async def handle_start_callback_query( - callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool + callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool, captcha: bool = False ): - await start_command(callback_query.message, state, session, admin) + await start_command(callback_query.message, state, session, admin, captcha) @router.message(Command("start")) -async def start_command(message: Message, state: FSMContext, session: Any, admin: bool): +async def start_command(message: Message, state: FSMContext, session: Any, admin: bool, captcha: bool = True): """Обрабатывает команду /start, включает логику рефералов и подарков.""" logger.info(f"Вызвана функция start_command для пользователя {message.chat.id}") await state.clear() # Проверка капчи, если включена - if CAPTCHA_ENABLE: - captcha = await generate_captcha(state) + if CAPTCHA_ENABLE and captcha: + captcha = await generate_captcha(message, state) await message.answer(text=captcha["text"], reply_markup=captcha["markup"]) return @@ -74,9 +74,7 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin if not connection_exists: await add_connection(tg_id=message.chat.id, session=session) - logger.info( - f"Пользователь {message.chat.id} успешно добавлен в базу данных." - ) + logger.info(f"Пользователь {message.chat.id} успешно добавлен в базу данных.") if "gift_" in message.text: logger.info(f"Обнаружена ссылка на подарок: {message.text}") @@ -91,36 +89,24 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin ) if gift_info is None: - logger.warning( - f"Подарок с ID {gift_id} уже был использован или не существует." - ) - await message.answer( - "Этот подарок уже был использован или не существует." - ) + logger.warning(f"Подарок с ID {gift_id} уже был использован или не существует.") + await message.answer("Этот подарок уже был использован или не существует.") return await show_start_menu(message, admin, session) if gift_info["sender_tg_id"] == recipient_tg_id: logger.warning( f"Пользователь {recipient_tg_id} попытался активировать подарок, который был отправлен им самим." ) - await message.answer( - "❌ Вы не можете получить подарок от самого себя." - ) + await message.answer("❌ Вы не можете получить подарок от самого себя.") return await show_start_menu(message, admin, session) selected_months = gift_info["selected_months"] expiry_time = gift_info["expiry_time"] expiry_time_naive = expiry_time.replace(tzinfo=None) - logger.info( - f"Подарок с ID {gift_id} успешно найден для пользователя {recipient_tg_id}." - ) + logger.info(f"Подарок с ID {gift_id} успешно найден для пользователя {recipient_tg_id}.") - await create_key( - recipient_tg_id, expiry_time_naive, state, session, message - ) - logger.info( - f"Ключ создан для пользователя {recipient_tg_id} на срок {selected_months} месяцев." - ) + await create_key(recipient_tg_id, expiry_time_naive, state, session, message) + logger.info(f"Ключ создан для пользователя {recipient_tg_id} на срок {selected_months} месяцев.") await session.execute( "UPDATE gifts SET is_used = TRUE, recipient_tg_id = $1 WHERE gift_id = $2", @@ -131,9 +117,7 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin await message.answer( f"🎉 Ваш подарок на {selected_months} {'месяц' if selected_months == 1 else 'месяца' if selected_months in [2, 3, 4] else 'месяцев'} активирован!" ) - logger.info( - f"Подарок на {selected_months} месяцев активирован для пользователя {recipient_tg_id}." - ) + logger.info(f"Подарок на {selected_months} месяцев активирован для пользователя {recipient_tg_id}.") return elif "referral_" in message.text: @@ -141,21 +125,13 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin referrer_tg_id = int(message.text.split("referral_")[1]) if connection_exists: - logger.info( - f"Пользователь {message.chat.id} уже зарегистрирован и не может стать рефералом." - ) - await message.answer( - "❌ Вы уже зарегистрированы и не можете использовать реферальную ссылку." - ) + logger.info(f"Пользователь {message.chat.id} уже зарегистрирован и не может стать рефералом.") + await message.answer("❌ Вы уже зарегистрированы и не можете использовать реферальную ссылку.") return await show_start_menu(message, admin, session) if referrer_tg_id == message.chat.id: - logger.warning( - f"Пользователь {message.chat.id} попытался стать рефералом самого себя." - ) - await message.answer( - "❌ Вы не можете быть рефералом самого себя." - ) + logger.warning(f"Пользователь {message.chat.id} попытался стать рефералом самого себя.") + await message.answer("❌ Вы не можете быть рефералом самого себя.") return await show_start_menu(message, admin, session) existing_referral = await session.fetchrow( @@ -168,9 +144,7 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin return await show_start_menu(message, admin, session) await add_referral(message.chat.id, referrer_tg_id, session) - logger.info( - f"Реферал {message.chat.id} использовал ссылку от пользователя {referrer_tg_id}" - ) + logger.info(f"Реферал {message.chat.id} использовал ссылку от пользователя {referrer_tg_id}") return await show_start_menu(message, admin, session) except (ValueError, IndexError) as e: @@ -178,16 +152,12 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin return else: - logger.info( - f"Пользователь {message.chat.id} зашел без реферальной ссылки или подарка." - ) + logger.info(f"Пользователь {message.chat.id} зашел без реферальной ссылки или подарка.") await show_start_menu(message, admin, session) except (ValueError, IndexError) as e: - logger.error( - f"Ошибка при обработке сообщения пользователя {message.chat.id}: {e}" - ) + logger.error(f"Ошибка при обработке сообщения пользователя {message.chat.id}: {e}") await message.answer("❌ Произошла ошибка. Пожалуйста, попробуйте снова.") else: await show_start_menu(message, admin, session) @@ -202,26 +172,20 @@ async def show_start_menu(message: Message, admin: bool, session: Any): builder = InlineKeyboardBuilder() if trial_status == 0: - builder.row( - InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn") - ) + builder.row(InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn")) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) if CHANNEL_EXISTS: builder.row( InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL), - InlineKeyboardButton(text="📢 Канал", url=CHANNEL_URL) + InlineKeyboardButton(text="📢 Канал", url=CHANNEL_URL), ) else: - builder.row( - InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL) - ) + builder.row(InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL)) if admin: - builder.row( - InlineKeyboardButton(text="🔧 Администратор", callback_data="admin") - ) + builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")) builder.row(InlineKeyboardButton(text="🌐 О VPN", callback_data="about_vpn")) @@ -267,24 +231,20 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session: Any): builder.row( InlineKeyboardButton( text=IMPORT_IOS, - url=f'{CONNECT_IOS}{trial_key_info["key"]}', + url=f"{CONNECT_IOS}{trial_key_info['key']}", ), InlineKeyboardButton( text=IMPORT_ANDROID, - url=f'{CONNECT_ANDROID}{trial_key_info["key"]}', + url=f"{CONNECT_ANDROID}{trial_key_info['key']}", ), ) builder.row( InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{email}"), InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}"), ) - builder.row( - InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") - ) + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - await callback_query.message.answer( - key_message, reply_markup=builder.as_markup() - ) + await callback_query.message.answer(key_message, reply_markup=builder.as_markup()) @router.callback_query(F.data == "about_vpn") @@ -292,9 +252,7 @@ async def handle_about_vpn(callback_query: CallbackQuery): builder = InlineKeyboardBuilder() if DONATIONS_ENABLE: - builder.row( - InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate") - ) + builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")) builder.row( InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL), @@ -305,6 +263,4 @@ async def handle_about_vpn(callback_query: CallbackQuery): ) builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="start")) - await callback_query.message.answer( - get_about_vpn("3.2.3-minor"), reply_markup=builder.as_markup() - ) + await callback_query.message.answer(get_about_vpn("3.2.3-minor"), reply_markup=builder.as_markup()) diff --git a/handlers/user.py b/handlers/user.py index 5e183db8..5fc65f9c 100644 --- a/handlers/user.py +++ b/handlers/user.py @@ -12,7 +12,9 @@ router = Router() @router.my_chat_member(ChatMemberUpdatedFilter(member_status_changed=KICKED)) async def user_blocked_bot(event: ChatMemberUpdated, session: Any): logger.info(f"User {event.from_user.id} blocked the bot.") - await session.execute("INSERT INTO blocked_users (tg_id) VALUES ($1) ON CONFLICT (tg_id) DO NOTHING", event.from_user.id) + await session.execute( + "INSERT INTO blocked_users (tg_id) VALUES ($1) ON CONFLICT (tg_id) DO NOTHING", event.from_user.id + ) @router.my_chat_member(ChatMemberUpdatedFilter(member_status_changed=MEMBER)) diff --git a/handlers/utils.py b/handlers/utils.py index c482ff7f..608a0f4c 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -14,9 +14,7 @@ from logger import logger async def get_usd_rate(): try: async with aiohttp.ClientSession() as session: - async with session.get( - "https://www.cbr-xml-daily.ru/daily_json.js" - ) as response: + async with session.get("https://www.cbr-xml-daily.ru/daily_json.js") as response: if response.status == 200: data = await response.text() usd = float(json.loads(data)["Valute"]["USD"]["Value"]) @@ -86,9 +84,7 @@ async def get_least_loaded_cluster() -> str: return least_loaded_cluster -async def handle_error( - tg_id: int, callback_query: object | None = None, message: str = "" -) -> None: +async def handle_error(tg_id: int, callback_query: object | None = None, message: str = "") -> None: """ Обрабатывает ошибку, отправляя сообщение пользователю. @@ -100,9 +96,7 @@ async def handle_error( try: if callback_query and hasattr(callback_query, "message"): try: - await bot.delete_message( - chat_id=tg_id, message_id=callback_query.message.message_id - ) + await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) except Exception as delete_error: logger.warning(f"Не удалось удалить сообщение: {delete_error}") diff --git a/middlewares/delete.py b/middlewares/delete.py index 846ed89a..5c88f29b 100644 --- a/middlewares/delete.py +++ b/middlewares/delete.py @@ -16,9 +16,7 @@ class DeleteMessageMiddleware(BaseMiddleware): if isinstance(event, Message): if not event.text or not event.text.startswith("/start"): try: - await event.bot.delete_message( - event.chat.id, event.message_id - 1 - ) + await event.bot.delete_message(event.chat.id, event.message_id - 1) except Exception: pass await event.delete() diff --git a/middlewares/throttling.py b/middlewares/throttling.py index 8fd18052..d1085dba 100644 --- a/middlewares/throttling.py +++ b/middlewares/throttling.py @@ -59,8 +59,6 @@ class ThrottlingMiddleware(BaseMiddleware): ) self.caches[key][user.id] = None else: - logger.debug( - f"No throttling key provided for user {user.id}, proceeding without throttle." - ) + logger.debug(f"No throttling key provided for user {user.id}, proceeding without throttle.") return await handler(event, data) diff --git a/pyproject.toml b/pyproject.toml index e8e30a7a..69cbd0f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ exclude = [ [tool.ruff.format] quote-style = "double" -indent-style = "tab" +indent-style = "space" [tool.darker] src = ["."] diff --git a/servers.py b/servers.py index a5920cc6..4a2b0b81 100644 --- a/servers.py +++ b/servers.py @@ -16,9 +16,7 @@ try: from config import CLUSTERS except ImportError: CLUSTERS = None - logger.warning( - "Переменная CLUSTERS не найдена в конфигурации. Добавьте сервера через админ-панель!" - ) + logger.warning("Переменная CLUSTERS не найдена в конфигурации. Добавьте сервера через админ-панель!") async def sync_servers_with_db(): @@ -27,9 +25,7 @@ async def sync_servers_with_db(): Если CLUSTERS не найден, синхронизация не будет выполнена. """ if CLUSTERS is None: - logger.info( - "Конфигурация CLUSTERS не найдена. Синхронизация не будет выполнена." - ) + logger.info("Конфигурация CLUSTERS не найдена. Синхронизация не будет выполнена.") return try: @@ -59,13 +55,9 @@ async def sync_servers_with_db(): server_info["SUBSCRIPTION"], server_info["INBOUND_ID"], ) - logger.info( - f"Сервер {server_info['name']} из кластера {cluster_name} добавлен в базу данных." - ) + logger.info(f"Сервер {server_info['name']} из кластера {cluster_name} добавлен в базу данных.") else: - logger.info( - f"Сервер {server_info['name']} из кластера {cluster_name} уже существует." - ) + logger.info(f"Сервер {server_info['name']} из кластера {cluster_name} уже существует.") except Exception as e: logger.error(f"Ошибка при синхронизации серверов: {e}") @@ -104,25 +96,14 @@ async def notify_admin(server_name: str): current_time = datetime.now() last_notification_time = last_notification_times.get(server_name) - if ( - last_notification_time - and current_time - last_notification_time < timedelta(minutes=3) - ): - logger.info( - f"Не отправляем уведомление для сервера {server_name}, так как прошло менее 3 минут." - ) + if last_notification_time and current_time - last_notification_time < timedelta(minutes=3): + logger.info(f"Не отправляем уведомление для сервера {server_name}, так как прошло менее 3 минут.") return - logger.info( - f"Отправка уведомлений администратору о недоступности сервера {server_name}..." - ) + logger.info(f"Отправка уведомлений администратору о недоступности сервера {server_name}...") builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="Управление сервером", callback_data=f"manage_server|{server_name}" - ) - ) + builder.row(InlineKeyboardButton(text="Управление сервером", callback_data=f"manage_server|{server_name}")) for admin_id in ADMIN_ID: await bot.send_message( @@ -134,9 +115,7 @@ async def notify_admin(server_name: str): ), reply_markup=builder.as_markup(), ) - logger.info( - f"Уведомление отправлено администратору с ID {admin_id} о сервере {server_name}." - ) + logger.info(f"Уведомление отправлено администратору с ID {admin_id} о сервере {server_name}.") last_notification_times[server_name] = current_time except Exception as e: @@ -160,9 +139,7 @@ async def check_servers(): server_name = server["server_name"] server_host = extract_host(original_api_url) - logger.debug( - f"Проверка доступности сервера '{server_name}' с хостом {server_host}" - ) + logger.debug(f"Проверка доступности сервера '{server_name}' с хостом {server_host}") is_online = await ping_server(server_host) @@ -170,18 +147,12 @@ async def check_servers(): last_ping_times[server_name] = current_time else: last_ping_time = last_ping_times.get(server_name) - if last_ping_time and current_time - last_ping_time > timedelta( - minutes=3 - ): - logger.warning( - f"Сервер {server_name} не отвечает более 3 минут. Отправляю уведомление." - ) + if last_ping_time and current_time - last_ping_time > timedelta(minutes=3): + logger.warning(f"Сервер {server_name} не отвечает более 3 минут. Отправляю уведомление.") await notify_admin(server_name) elif not last_ping_time: last_ping_times[server_name] = current_time - logger.info( - f"Сервер {server_name} не отвечал ранее, но теперь зарегистрирован." - ) + logger.info(f"Сервер {server_name} не отвечал ранее, но теперь зарегистрирован.") logger.info("Завершена проверка всех серверов.") await asyncio.sleep(PING_TIME) From 8667ae3a23a148c1883107cd8187223c94c28b5a Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Tue, 21 Jan 2025 12:03:56 +0300 Subject: [PATCH 15/49] Refactor file handling to use aiofiles for asynchronous operations - Replaced synchronous file reading with asynchronous file handling using aiofiles in multiple handlers and the backup module. - Improved performance and responsiveness when sending images and backups by utilizing async file operations. - Ensured consistent use of BufferedInputFile across all relevant functions for better code maintainability. --- backup.py | 7 ++++--- handlers/instructions/instructions.py | 7 ++++--- handlers/keys/keys.py | 12 +++++++----- handlers/profile.py | 18 +++++++++++------- handlers/start.py | 7 ++++--- 5 files changed, 30 insertions(+), 21 deletions(-) diff --git a/backup.py b/backup.py index c97c4364..a36e4d31 100644 --- a/backup.py +++ b/backup.py @@ -1,7 +1,7 @@ import os import subprocess from datetime import datetime - +import aiofiles from aiogram.types import BufferedInputFile from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT @@ -58,8 +58,9 @@ def _create_database_backup(): async def _send_backup_to_admin(bot, backup_file_path): try: - with open(backup_file_path, "rb") as backup_file: - backup_input_file = BufferedInputFile(backup_file.read(), filename=os.path.basename(backup_file_path)) + async with aiofiles.open(backup_file_path, "rb") as backup_file: + backup_file_data = await backup_file.read() + backup_input_file = BufferedInputFile(backup_file_data, filename=os.path.basename(backup_file_path)) admin_ids: int | list[int] = ADMIN_ID if isinstance(admin_ids, list): for id in admin_ids: diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index 3b278264..2686c24d 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -1,6 +1,6 @@ import os from typing import Any - +import aiofiles import asyncpg from aiogram import F, Router, types from aiogram.types import BufferedInputFile, InlineKeyboardButton @@ -45,9 +45,10 @@ async def send_instructions( else: send_photo = callback_query_or_message.answer_photo - with open(image_path, "rb") as image_from_buffer: + async with aiofiles.open(image_path, "rb") as image_from_buffer: + image_data = await image_from_buffer.read() await send_photo( - BufferedInputFile(image_from_buffer.read(), filename="instructions.jpg"), + BufferedInputFile(image_data, filename="instructions.jpg"), caption=instructions_message, reply_markup=builder.as_markup(), ) diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 5fdc6f16..191e1efa 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -3,7 +3,7 @@ import locale import os from datetime import datetime, timedelta from typing import Any - +import aiofiles from aiogram import F, Router, types from aiogram.types import BufferedInputFile, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder @@ -123,9 +123,10 @@ async def send_with_optional_image(send_message, send_photo, image_path, text, k Отправляет сообщение с изображением, если файл существует. В противном случае отправляет только текст. """ if os.path.isfile(image_path): - with open(image_path, "rb") as image_file: + async with aiofiles.open(image_path, "rb") as image_file: + image_data = await image_file.read() await send_photo( - photo=BufferedInputFile(image_file.read(), filename=os.path.basename(image_path)), + photo=BufferedInputFile(image_data, filename=os.path.basename(image_path)), caption=text, reply_markup=keyboard, ) @@ -219,9 +220,10 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session await callback_query.message.answer("Файл изображения не найден.") return - with open(image_path, "rb") as image_file: + async with aiofiles.open(image_path, "rb") as image_file: + image_data = await image_file.read() await callback_query.message.answer_photo( - photo=BufferedInputFile(image_file.read(), filename="pic_view.jpg"), + photo=BufferedInputFile(image_data, filename="pic_view.jpg"), caption=response_message, reply_markup=keyboard, ) diff --git a/handlers/profile.py b/handlers/profile.py index 185eb793..6e7a3126 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -3,6 +3,7 @@ from typing import Any import asyncpg from aiogram import F, Router, types +import aiofiles from aiogram.fsm.context import FSMContext from aiogram.types import BufferedInputFile, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder @@ -83,16 +84,17 @@ async def process_callback_view_profile( builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="start")) if os.path.isfile(image_path): - with open(image_path, "rb") as image_file: + async with aiofiles.open(image_path, "rb") as image_file: + image_data = await image_file.read() if is_callback: await callback_query_or_message.message.answer_photo( - photo=BufferedInputFile(image_file.read(), filename="pic.jpg"), + photo=BufferedInputFile(image_data, filename="pic.jpg"), caption=profile_message, reply_markup=builder.as_markup(), ) else: await callback_query_or_message.answer_photo( - photo=BufferedInputFile(image_file.read(), filename="pic.jpg"), + photo=BufferedInputFile(image_data, filename="pic.jpg"), caption=profile_message, reply_markup=builder.as_markup(), ) @@ -173,9 +175,10 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery): ) if os.path.isfile(image_path): - with open(image_path, "rb") as image_file: + async with aiofiles.open(image_path, "rb") as image_file: + image_data = await image_file.read() await callback_query.message.answer_photo( - photo=BufferedInputFile(image_file.read(), filename="tariffs.jpg"), + photo=BufferedInputFile(image_data, filename="tariffs.jpg"), caption=tariffs_message, reply_markup=builder.as_markup(), ) @@ -200,9 +203,10 @@ async def invite_handler(callback_query: types.CallbackQuery): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) if os.path.isfile(image_path): - with open(image_path, "rb") as image_file: + async with aiofiles.open(image_path, "rb") as image_file: + image_data = await image_file.read() await callback_query.message.answer_photo( - photo=BufferedInputFile(image_file.read(), filename="pic_invite.jpg"), + photo=BufferedInputFile(image_data, filename="pic_invite.jpg"), caption=invite_message, reply_markup=builder.as_markup(), ) diff --git a/handlers/start.py b/handlers/start.py index d7560ac3..f834ffda 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -1,6 +1,6 @@ import os from typing import Any - +import aiofiles from aiogram import F, Router from aiogram.filters import Command from aiogram.fsm.context import FSMContext @@ -190,9 +190,10 @@ async def show_start_menu(message: Message, admin: bool, session: Any): builder.row(InlineKeyboardButton(text="🌐 О VPN", callback_data="about_vpn")) if os.path.isfile(image_path): - with open(image_path, "rb") as image_from_buffer: + async with aiofiles.open(image_path, "rb") as image_from_buffer: + image_data = await image_from_buffer.read() await message.answer_photo( - photo=BufferedInputFile(image_from_buffer.read(), filename="pic.jpg"), + photo=BufferedInputFile(image_data, filename="pic.jpg"), caption=WELCOME_TEXT, reply_markup=builder.as_markup(), ) From c87ec273e5383d66b5ca01c69a50db91305ee51a Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Wed, 22 Jan 2025 10:31:03 +0300 Subject: [PATCH 16/49] Add directory creation for database backups in backup.py --- backup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backup.py b/backup.py index a36e4d31..326dd183 100644 --- a/backup.py +++ b/backup.py @@ -25,6 +25,10 @@ def _create_database_backup(): PORT = PG_PORT BACKUP_DIR = BACK_DIR DATE = datetime.now().strftime("%Y-%m-%d-%H%M%S") + + if not os.path.exists(BACKUP_DIR): + os.makedirs(BACKUP_DIR) + BACKUP_FILE = f"{BACKUP_DIR}/{DB_NAME}-backup-{DATE}.sql" os.environ["PGPASSWORD"] = DB_PASSWORD From 3af4afd159a4bcc3cdebaf0003c168944cfbae69 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 23 Jan 2025 05:49:24 +0300 Subject: [PATCH 17/49] Update balance retrieval to round values to one decimal place in database.py --- database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database.py b/database.py index a2d1a9ca..56a056e2 100644 --- a/database.py +++ b/database.py @@ -474,7 +474,7 @@ async def get_balance(tg_id: int) -> float: conn = await asyncpg.connect(DATABASE_URL) balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) logger.info(f"Получен баланс для пользователя {tg_id}: {balance}") - return balance if balance is not None else 0.0 + return round(balance, 1) if balance is not None else 0.0 except Exception as e: logger.error(f"Ошибка при получении баланса для пользователя {tg_id}: {e}") return 0.0 From b78e1148d1ff1f5e2dc654a1de39879add426901 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 23 Jan 2025 06:14:48 +0300 Subject: [PATCH 18/49] Update .gitignore to include logs and setup.py, and remove unnecessary entries --- .gitignore | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index fcb54e4b..148fafa2 100644 --- a/.gitignore +++ b/.gitignore @@ -54,5 +54,6 @@ scripts models.py Dockerfile .csv - -setup.py \ No newline at end of file +/logs +setup.py +.ruff_cache \ No newline at end of file From 845a8c8671ff482098bb33f4f0d23b35bac83f0c Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 23 Jan 2025 03:15:11 +0000 Subject: [PATCH 19/49] Auto-format code with Ruff using pyproject.toml --- database.py | 4 +--- handlers/keys/key_management.py | 10 ++-------- handlers/keys/key_utils.py | 5 +---- handlers/keys/keys.py | 28 +++++++--------------------- handlers/notifications.py | 14 +++----------- 5 files changed, 14 insertions(+), 47 deletions(-) diff --git a/database.py b/database.py index bb934f91..e226f8d5 100644 --- a/database.py +++ b/database.py @@ -776,9 +776,7 @@ async def get_referral_stats(referrer_tg_id: int): referrer_tg_id, ) - logger.debug( - f"Получена общая сумма бонусов от рефералов: {total_referral_bonus}" - ) + logger.debug(f"Получена общая сумма бонусов от рефералов: {total_referral_bonus}") return { "total_referrals": total_referrals, diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 2ec4763b..3ab75926 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -189,11 +189,7 @@ async def create_key( builder = InlineKeyboardBuilder() for country in countries: callback_data = f"select_country|{country}|{expiry_time.isoformat()}" - builder.row( - InlineKeyboardButton( - text=country, callback_data=callback_data - ) - ) + builder.row(InlineKeyboardButton(text=country, callback_data=callback_data)) logger.info(f"[Country Selection] Добавлена кнопка для страны: {country} с callback_data: {callback_data}") builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="profile")) @@ -363,9 +359,7 @@ async def finalize_key_creation( ) if not existing_key: break - logger.warning( - f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one." - ) + logger.warning(f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one.") client_id = str(uuid.uuid4()) email = key_name.lower() diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index 0935b478..f59b3072 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -73,9 +73,7 @@ async def create_client_on_server( server_name = server_info.get("server_name", "unknown") if not inbound_id: - logger.warning( - f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск." - ) + logger.warning(f"INBOUND_ID отсутствует для сервера {server_name}. Пропуск.") return if SUPERNODE: @@ -103,7 +101,6 @@ async def create_client_on_server( await asyncio.sleep(0.7) - async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, total_gb): try: servers = await get_servers_from_db() diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 751317c3..7b8261df 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -114,9 +114,7 @@ def build_keys_response(records): if expiry_time: expiry_date_full = datetime.fromtimestamp(expiry_time / 1000, tz=moscow_tz) - formatted_date_full = expiry_date_full.strftime( - "до %d %B %Y года, %H:%M" - ).lower() + formatted_date_full = expiry_date_full.strftime("до %d %B %Y года, %H:%M").lower() formatted_date_short = expiry_date_full.strftime("до %d %B").lower() else: @@ -124,23 +122,16 @@ def build_keys_response(records): formatted_date_short = "без срока действия" button_text = f"{key_name} ({formatted_date_short})" - builder.row( - InlineKeyboardButton( - text=button_text, callback_data=f"view_key|{key_name}" - ) - ) + builder.row(InlineKeyboardButton(text=button_text, callback_data=f"view_key|{key_name}")) response_message += f"• {key_name} ({formatted_date_full})\n" else: response_message = ( - "🔑 У вас пока нет подписок.\n\n" - "Вы можете создать новую подписку для подключения устройств." + "🔑 У вас пока нет подписок.\n\nВы можете создать новую подписку для подключения устройств." ) - builder.row( - InlineKeyboardButton(text="➕ Добавить подписку", callback_data="create_key") - ) + builder.row(InlineKeyboardButton(text="➕ Добавить подписку", callback_data="create_key")) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) inline_keyboard = builder.as_markup() @@ -380,9 +371,8 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio months = plan_details["months"] price = plan_details["price"] discount = DISCOUNTS.get(plan_id, 0) - button_text = ( - f'📅 {months} месяц{"а" if months > 1 else ""} ({price} руб.)' - + (f' {discount}% скидка' if discount > 0 else "") + button_text = f"📅 {months} месяц{'а' if months > 1 else ''} ({price} руб.)" + ( + f" {discount}% скидка" if discount > 0 else "" ) builder.row( InlineKeyboardButton( @@ -391,11 +381,7 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio ) ) - builder.row( - InlineKeyboardButton( - text="🔙 Назад", callback_data="view_keys" - ) - ) + builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="view_keys")) balance = await get_balance(tg_id) diff --git a/handlers/notifications.py b/handlers/notifications.py index 1a242e2b..c01bb371 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -187,7 +187,6 @@ async def process_10h_record(record, bot, conn): await send_renewal_notification(bot, tg_id, email, message, conn, record["client_id"], "notified") - async def notify_24h_keys( bot: Bot, conn: asyncpg.Connection, @@ -327,12 +326,7 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): username = user["username"] first_name = user["first_name"] last_name = user["last_name"] - display_name = ( - username - or first_name - or last_name - or "Пользователь" - ) + display_name = username or first_name or last_name or "Пользователь" try: can_notify = await check_notification_time(tg_id, "inactive_trial", hours=24, session=conn) @@ -407,9 +401,7 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: for record in expired_keys: try: await delete_key_from_cluster(record["server_id"], record["email"], record["email"]) - await conn.execute( - "DELETE FROM keys WHERE client_id = $1", record["client_id"] - ) + await conn.execute("DELETE FROM keys WHERE client_id = $1", record["client_id"]) logger.info(f"Удалён истёкший ключ {record['client_id']} пользователя {record['tg_id']}.") except Exception as e: logger.error(f"Ошибка при удалении истёкшего ключа {record['client_id']}: {e}") @@ -468,7 +460,7 @@ async def process_key(record, bot, conn): await bot.send_photo( tg_id, photo=BufferedInputFile(image_file.read(), filename="notify_expired.jpg"), - caption = KEY_RENEWED.format(email=email), + caption=KEY_RENEWED.format(email=email), reply_markup=keyboard, ) else: From 311c48edcd295ed350777d19866482e49b188410 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 23 Jan 2025 06:34:45 +0300 Subject: [PATCH 20/49] Refactor code to utilize aiofiles for asynchronous file handling across multiple modules. Added a new function to remove blocked users from the database and updated user blocking/unblocking logic to use this new function. Improved database interaction in admin panel handlers by passing session objects instead of creating new connections. This enhances performance and maintains consistency in file operations. --- backup.py | 1 + database.py | 13 +++++++++++++ handlers/admin/admin_panel.py | 22 +++++++--------------- handlers/instructions/instructions.py | 1 + handlers/keys/keys.py | 1 + handlers/profile.py | 2 +- handlers/start.py | 1 + handlers/user.py | 7 +++---- 8 files changed, 28 insertions(+), 20 deletions(-) diff --git a/backup.py b/backup.py index 326dd183..c0a55e0a 100644 --- a/backup.py +++ b/backup.py @@ -1,6 +1,7 @@ import os import subprocess from datetime import datetime + import aiofiles from aiogram.types import BufferedInputFile diff --git a/database.py b/database.py index e226f8d5..6c3f1958 100644 --- a/database.py +++ b/database.py @@ -43,6 +43,19 @@ async def add_blocked_user(tg_id: int, conn: asyncpg.Connection): ) +async def remove_blocked_user(tg_id: int | list[int], conn: asyncpg.Connection): + """ + Удаляет пользователя или список пользователей из списка заблокированных. + + :param tg_id: ID пользователя Telegram или список ID + :param conn: Подключение к базе данных + """ + if isinstance(tg_id, list): + await conn.execute("DELETE FROM blocked_users WHERE tg_id = ANY($1)", tg_id) + else: + await conn.execute("DELETE FROM blocked_users WHERE tg_id = $1", tg_id) + + async def init_db(file_path: str = "assets/schema.sql"): with open(file_path, mode="r") as file: sql_content = file.read() diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index 71dba4e4..81d52d52 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -3,7 +3,6 @@ from datetime import datetime from io import BytesIO from typing import Any -import asyncpg from aiogram import F, Router, types from aiogram.filters import Command from aiogram.fsm.context import FSMContext @@ -13,8 +12,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from backup import backup_database from bot import bot -from config import DATABASE_URL -from database import delete_user_data +from database import delete_user_data, remove_blocked_user from filters.admin import IsAdminFilter from logger import logger @@ -443,10 +441,9 @@ async def handle_ban_user(callback_query: types.CallbackQuery): @router.callback_query(F.data == "export_to_csv") -async def export_banned_users_to_csv(callback_query: types.CallbackQuery): - conn = await asyncpg.connect(DATABASE_URL) +async def export_banned_users_to_csv(callback_query: types.CallbackQuery, session: Any): try: - banned_users = await conn.fetch("SELECT tg_id, blocked_at FROM blocked_users") + banned_users = await session.fetch("SELECT tg_id, blocked_at FROM blocked_users") import csv import io @@ -476,15 +473,12 @@ async def export_banned_users_to_csv(callback_query: types.CallbackQuery): text=f"Ошибка при выгрузке CSV: {e}", reply_markup=builder.as_markup(), ) - finally: - await conn.close() @router.callback_query(F.data == "delete_banned_users") -async def delete_banned_users(callback_query: types.CallbackQuery): - conn = await asyncpg.connect(DATABASE_URL) +async def delete_banned_users(callback_query: types.CallbackQuery, session: Any): try: - blocked_users = await conn.fetch("SELECT tg_id FROM blocked_users") + blocked_users = await session.fetch("SELECT tg_id FROM blocked_users") blocked_ids = [record["tg_id"] for record in blocked_users] if not blocked_ids: @@ -492,9 +486,9 @@ async def delete_banned_users(callback_query: types.CallbackQuery): return for tg_id in blocked_ids: - await delete_user_data(conn, tg_id) + await delete_user_data(session, tg_id) - await conn.execute("DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids) + await remove_blocked_user(blocked_ids, session) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management")) @@ -509,5 +503,3 @@ async def delete_banned_users(callback_query: types.CallbackQuery): text=f"Ошибка при удалении записей: {e}", reply_markup=builder.as_markup(), ) - finally: - await conn.close() diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index 2686c24d..9663531d 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -1,5 +1,6 @@ import os from typing import Any + import aiofiles import asyncpg from aiogram import F, Router, types diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 7b8261df..a0b4c6b5 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -3,6 +3,7 @@ import locale import os from datetime import datetime, timedelta from typing import Any + import aiofiles import asyncpg import pytz diff --git a/handlers/profile.py b/handlers/profile.py index 6e7a3126..82023a45 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -1,9 +1,9 @@ import os from typing import Any +import aiofiles import asyncpg from aiogram import F, Router, types -import aiofiles from aiogram.fsm.context import FSMContext from aiogram.types import BufferedInputFile, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder diff --git a/handlers/start.py b/handlers/start.py index 866890e4..a48f02d6 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -1,5 +1,6 @@ import os from typing import Any + import aiofiles from aiogram import F, Router from aiogram.filters import Command diff --git a/handlers/user.py b/handlers/user.py index 5fc65f9c..bc5e590d 100644 --- a/handlers/user.py +++ b/handlers/user.py @@ -4,6 +4,7 @@ from aiogram import Router from aiogram.filters.chat_member_updated import KICKED, MEMBER, ChatMemberUpdatedFilter from aiogram.types import ChatMemberUpdated +from database import add_blocked_user, remove_blocked_user from logger import logger router = Router() @@ -12,12 +13,10 @@ router = Router() @router.my_chat_member(ChatMemberUpdatedFilter(member_status_changed=KICKED)) async def user_blocked_bot(event: ChatMemberUpdated, session: Any): logger.info(f"User {event.from_user.id} blocked the bot.") - await session.execute( - "INSERT INTO blocked_users (tg_id) VALUES ($1) ON CONFLICT (tg_id) DO NOTHING", event.from_user.id - ) + await add_blocked_user(event.from_user.id, session) @router.my_chat_member(ChatMemberUpdatedFilter(member_status_changed=MEMBER)) async def user_unblocked_bot(event: ChatMemberUpdated, session: Any): logger.info(f"User {event.from_user.id} unblocked the bot.") - await session.execute("DELETE FROM blocked_users WHERE tg_id = $1", event.from_user.id) + await remove_blocked_user(event.from_user.id, session) From 2d638b0c8273f5bd2347e652a13a35c3b1b3526f Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 23 Jan 2025 07:49:21 +0300 Subject: [PATCH 21/49] Refactor key management functions to improve database interactions by consolidating key deletion logic into a single function. Introduce timezone-aware expiry date calculations for keys and enhance key retrieval processes across various handlers. Update the start command to utilize a dedicated function for adding connections, ensuring cleaner code and better session management. --- database.py | 104 +++++++++++++++++--------- handlers/admin/admin_user_editor.py | 46 +----------- handlers/instructions/instructions.py | 32 ++------ handlers/keys/key_management.py | 7 +- handlers/keys/key_utils.py | 7 -- handlers/keys/keys.py | 34 ++------- handlers/keys/subscriptions.py | 6 +- handlers/keys/trial_key.py | 4 +- handlers/notifications.py | 4 +- handlers/start.py | 9 +-- 10 files changed, 95 insertions(+), 158 deletions(-) diff --git a/database.py b/database.py index 6c3f1958..8dba5f4a 100644 --- a/database.py +++ b/database.py @@ -3,6 +3,7 @@ from datetime import datetime from typing import Any import asyncpg +import pytz from config import DATABASE_URL, REFERRAL_BONUS_PERCENTAGES from logger import logger @@ -57,7 +58,7 @@ async def remove_blocked_user(tg_id: int | list[int], conn: asyncpg.Connection): async def init_db(file_path: str = "assets/schema.sql"): - with open(file_path, mode="r") as file: + with open(file_path) as file: sql_content = file.read() statements = [stmt.strip() for stmt in sql_content.split(";") if stmt.strip()] @@ -843,39 +844,6 @@ async def update_key_expiry(client_id: str, new_expiry_time: int): logger.info("Закрытие подключения к базе данных") -async def delete_key(client_id: str): - """ - Удаление ключа из базы данных для указанного клиента. - - Args: - client_id (str): Уникальный идентификатор клиента, ключ которого будет удален - - Raises: - Exception: В случае ошибки при подключении к базе данных или удалении ключа - """ - conn = None - try: - conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для удаления ключа клиента {client_id}") - - await conn.execute( - """ - DELETE FROM keys - WHERE client_id = $1 - """, - client_id, - ) - logger.info(f"Успешно удален ключ для клиента {client_id}") - - except Exception as e: - logger.error(f"Ошибка при удалении ключа для клиента {client_id}: {e}") - raise - finally: - if conn: - await conn.close() - logger.info("Закрытие подключения к базе данных") - - async def add_balance_to_client(client_id: str, amount: float): """ Добавление баланса клиенту по его идентификатору Telegram. @@ -1202,7 +1170,7 @@ async def delete_user_data(session: Any, tg_id: int): await session.execute("DELETE FROM payments WHERE tg_id = $1", tg_id) await session.execute("DELETE FROM users WHERE tg_id = $1", tg_id) await session.execute("DELETE FROM connections WHERE tg_id = $1", tg_id) - await session.execute("DELETE FROM keys WHERE tg_id = $1", tg_id) + await delete_key(tg_id, session) await session.execute("DELETE FROM referrals WHERE referrer_tg_id = $1", tg_id) @@ -1261,3 +1229,69 @@ async def store_gift_link( finally: if conn is not None and session is None: await conn.close() + + +async def get_key_details(email, session): + record = await session.fetchrow( + """ + SELECT k.key, k.expiry_time, k.server_id,k, k.client_id, k.created_at, c.tg_id, c.balance + FROM keys k + JOIN connections c ON k.tg_id = c.tg_id + WHERE k.email = $1 + """, + email, + ) + + if not record: + return None + + cluster_name = record["server_id"] + + moscow_tz = pytz.timezone("Europe/Moscow") + expiry_date = datetime.fromtimestamp(record["expiry_time"] / 1000, tz=moscow_tz) + current_date = datetime.now(moscow_tz) + time_left = expiry_date - current_date + + if time_left.total_seconds() <= 0: + days_left_message = "Ключ истек." + elif time_left.days > 0: + days_left_message = f"Осталось дней: {time_left.days}" + else: + hours_left = time_left.seconds // 3600 + days_left_message = f"Осталось часов: {hours_left}" + + return { + "key": record["key"], + "created_at": record["created_at"], + "expiry_time": record["expiry_time"], + "client_id": record["client_id"], + "expiry_date": expiry_date.strftime("%d %B %Y года %H:%M"), + "days_left_message": days_left_message, + "server_name": cluster_name, + "balance": record["balance"], + "tg_id": record["tg_id"], + } + + +async def delete_key(identifier, session): + """ + Удаляет ключ из базы данных по client_id или tg_id + + Args: + identifier (str): client_id или tg_id для удаления + session: Сессия базы данных + + Raises: + Exception: В случае ошибки при удалении ключа + """ + try: + # Проверяем, является ли идентификатор числом (tg_id) или строкой (client_id) + if identifier.isdigit(): + query = "DELETE FROM keys WHERE tg_id = $1" + else: + query = "DELETE FROM keys WHERE client_id = $1" + + await session.execute(query, identifier) + logger.info(f"Ключ с идентификатором {identifier} успешно удалён") + except Exception as e: + logger.error(f"Ошибка при удалении ключа с идентификатором {identifier} из базы данных: {e}") diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index 94e02f33..64c7b6e1 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -13,6 +13,7 @@ from config import TOTAL_GB from database import ( delete_user_data, get_client_id_by_email, + get_key_details, get_servers_from_db, restore_trial, update_key_expiry, @@ -257,45 +258,6 @@ async def handle_new_balance_input(message: types.Message, state: FSMContext, se await state.clear() -async def get_key_details(email, session): - record = await session.fetchrow( - """ - SELECT k.key, k.expiry_time, k.server_id, c.tg_id, c.balance - FROM keys k - JOIN connections c ON k.tg_id = c.tg_id - WHERE k.email = $1 - """, - email, - ) - - if not record: - return None - - cluster_name = record["server_id"] - - moscow_tz = pytz.timezone("Europe/Moscow") - expiry_date = datetime.fromtimestamp(record["expiry_time"] / 1000, tz=moscow_tz) - current_date = datetime.now(moscow_tz) - time_left = expiry_date - current_date - - if time_left.total_seconds() <= 0: - days_left_message = "Ключ истек." - elif time_left.days > 0: - days_left_message = f"Осталось дней: {time_left.days}" - else: - hours_left = time_left.seconds // 3600 - days_left_message = f"Осталось часов: {hours_left}" - - return { - "key": record["key"], - "expiry_date": expiry_date.strftime("%d %B %Y года %H:%M"), - "days_left_message": days_left_message, - "server_name": cluster_name, - "balance": record["balance"], - "tg_id": record["tg_id"], - } - - @router.callback_query(F.data.startswith("edit_key_"), IsAdminFilter()) async def process_key_edit(callback_query: CallbackQuery, session: Any): email = callback_query.data.split("_", 2)[2] @@ -492,9 +454,9 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se @router.callback_query(F.data.startswith("delete_key_admin|"), IsAdminFilter()) async def process_callback_delete_key(callback_query: types.CallbackQuery, session: Any): email = callback_query.data.split("|")[1] - client_id = await session.fetchval("SELECT client_id FROM keys WHERE email = $1", email) + key_details = await get_key_details(email, session) - if client_id is None: + if key_details is None: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) await callback_query.message.answer("🔍 Ключ не найден. 🚫", reply_markup=builder.as_markup()) @@ -504,7 +466,7 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery, sessi builder.row( types.InlineKeyboardButton( text="✅ Да, удалить", - callback_data=f"confirm_delete_admin|{client_id}", + callback_data=f"confirm_delete_admin|{key_details['client_id']}", ) ) builder.row(types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="user_editor")) diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index 9663531d..dd373ccb 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -2,12 +2,12 @@ import os from typing import Any import aiofiles -import asyncpg from aiogram import F, Router, types from aiogram.types import BufferedInputFile, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from config import CONNECT_MACOS, CONNECT_WINDOWS, DATABASE_URL, SUPPORT_CHAT_URL +from config import CONNECT_MACOS, CONNECT_WINDOWS, SUPPORT_CHAT_URL +from database import get_key_details from handlers.texts import ( CONNECT_TV_TEXT, INSTRUCTION_PC, @@ -60,15 +60,7 @@ async def process_connect_pc(callback_query: types.CallbackQuery, session: Any): tg_id = callback_query.message.chat.id key_name = callback_query.data.split("|")[1] - record = await session.fetchrow( - """ - SELECT k.key - FROM keys k - WHERE k.tg_id = $1 AND k.email = $2 - """, - tg_id, - key_name, - ) + record = await get_key_details(key_name, session) if not record: await callback_query.message.answer("❌ Ключ не найден. Проверьте имя ключа. 🔍") @@ -107,27 +99,15 @@ async def process_connect_tv(callback_query: types.CallbackQuery): @router.callback_query(F.data.startswith("continue_tv|")) -async def process_continue_tv(callback_query: types.CallbackQuery): +async def process_continue_tv(callback_query: types.CallbackQuery, session: Any): key_name = callback_query.data.split("|")[1] tg_id = callback_query.from_user.id logger.info(f"tg_id: {tg_id}, key_name: {key_name}") - conn = await asyncpg.connect(DATABASE_URL) - try: - record = await conn.fetchrow( - """ - SELECT k.key - FROM keys k - WHERE k.tg_id = $1 AND k.email = $2 - """, - tg_id, - key_name, - ) + record = await get_key_details(key_name, session) - logger.info(f"Query result: {record}") - finally: - await conn.close() + logger.info(f"Query result: {record}") subscription_link = record["key"] diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 3ab75926..a06f004f 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -25,6 +25,7 @@ from config import ( ) from database import ( get_balance, + get_key_details, get_trial, save_temporary_data, store_key, @@ -352,11 +353,7 @@ async def finalize_key_creation( key_name = generate_random_email() logger.info(f"Generated random key name for user {tg_id}: {key_name}") - existing_key = await session.fetchrow( - "SELECT * FROM keys WHERE email = $1 AND tg_id = $2", - key_name, - tg_id, - ) + existing_key = await get_key_details(key_name, session) if not existing_key: break logger.warning(f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one.") diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index f59b3072..3657f77f 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -142,13 +142,6 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to raise e -async def delete_key_from_db(client_id, session): - try: - await session.execute("DELETE FROM keys WHERE client_id = $1", client_id) - except Exception as e: - logger.error(f"Ошибка при удалении ключа {client_id} из базы данных: {e}") - - async def delete_key_from_cluster(cluster_id, email, client_id): """Удаление ключа с серверов в кластере""" try: diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index a0b4c6b5..20c036fb 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -29,6 +29,7 @@ from config import ( from database import ( delete_key, get_balance, + get_key_details, get_servers_from_db, save_temporary_data, store_key, @@ -163,15 +164,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session tg_id = callback_query.message.chat.id key_name = callback_query.data.split("|")[1] try: - record = await session.fetchrow( - """ - SELECT k.expiry_time, k.server_id, k.key - FROM keys k - WHERE k.tg_id = $1 AND k.email = $2 - """, - tg_id, - key_name, - ) + record = await get_key_details(key_name, session) if record: key = record["key"] @@ -264,15 +257,7 @@ async def process_callback_update_subscription(callback_query: types.CallbackQue tg_id = callback_query.message.chat.id email = callback_query.data.split("|")[1] try: - record = await session.fetchrow( - """ - SELECT k.key, k.expiry_time, k.email, k.server_id, k.client_id - FROM keys k - WHERE k.tg_id = $1 AND k.email = $2 - """, - tg_id, - email, - ) + record = await get_key_details(email, session) if record: expiry_time = record["expiry_time"] @@ -353,14 +338,7 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio tg_id = callback_query.message.chat.id key_name = callback_query.data.split("|")[1] try: - record = await session.fetchrow( - """ - SELECT client_id, expiry_time - FROM keys - WHERE email = $1 - """, - key_name, - ) + record = await get_key_details(key_name, session) if record: client_id = record["client_id"] @@ -405,7 +383,7 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio async def process_callback_confirm_delete(callback_query: types.CallbackQuery, session: Any): email = callback_query.data.split("|")[1] try: - record = await session.fetchrow("SELECT client_id FROM keys WHERE email = $1", email) + record = await get_key_details(email, session) if record: client_id = record["client_id"] @@ -413,7 +391,7 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery, s back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_keys") keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) - await delete_key(client_id) + await delete_key(client_id, session) await callback_query.message.answer( response_message, reply_markup=keyboard, diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index 4f21ec14..d3f6d36e 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -7,7 +7,7 @@ import asyncpg from aiohttp import web from config import DATABASE_URL, PROJECT_NAME, SUB_MESSAGE, SUPERNODE, TRANSITION_DATE_STR -from database import get_servers_from_db +from database import get_key_details, get_servers_from_db from logger import logger # Глобальная переменная для пула соединений @@ -92,7 +92,7 @@ async def handle_old_subscription(request): await init_db_pool() async with db_pool.acquire() as conn: - key_info = await conn.fetchrow("SELECT created_at, server_id FROM keys WHERE email = $1", email) + key_info = await get_key_details(email, conn) if not key_info: logger.warning(f"Клиент с email {email} не найден в базе.") @@ -161,7 +161,7 @@ async def handle_new_subscription(request): await init_db_pool() async with db_pool.acquire() as conn: - client_data = await conn.fetchrow("SELECT tg_id, server_id FROM keys WHERE email = $1", email) + client_data = await get_key_details(email, conn) if not client_data: logger.warning(f"Клиент с email {email} не найден в базе.") diff --git a/handlers/keys/trial_key.py b/handlers/keys/trial_key.py index 9ec15090..00048577 100644 --- a/handlers/keys/trial_key.py +++ b/handlers/keys/trial_key.py @@ -8,7 +8,7 @@ from py3xui import AsyncApi from client import add_client from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, TRIAL_TIME -from database import get_servers_from_db, store_key, use_trial +from database import get_servers_from_db, get_trial, store_key, use_trial from handlers.texts import INSTRUCTIONS from handlers.utils import generate_random_email, get_least_loaded_cluster from logger import logger @@ -16,7 +16,7 @@ from logger import logger async def create_trial_key(tg_id: int, session: Any): try: - trial_status = await session.fetchval("SELECT trial FROM connections WHERE tg_id = $1", tg_id) + trial_status = await get_trial(tg_id, session) if trial_status == 1: return {"error": "Вы уже использовали пробную версию."} except Exception as e: diff --git a/handlers/notifications.py b/handlers/notifications.py index c01bb371..369e8e48 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -401,7 +401,7 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: for record in expired_keys: try: await delete_key_from_cluster(record["server_id"], record["email"], record["email"]) - await conn.execute("DELETE FROM keys WHERE client_id = $1", record["client_id"]) + await delete_key(record["client_id"], conn) logger.info(f"Удалён истёкший ключ {record['client_id']} пользователя {record['tg_id']}.") except Exception as e: logger.error(f"Ошибка при удалении истёкшего ключа {record['client_id']}: {e}") @@ -497,7 +497,7 @@ async def process_key(record, bot, conn): logger.error(f"Ошибка при удалении клиента {client_id} из кластера {cluster_id}: {e}") try: - await delete_key(client_id) + await delete_key(client_id, conn) logger.info(f"Ключ {client_id} удалён из базы данных.") except Exception as e: logger.error(f"Ошибка при удалении ключа {client_id} из базы данных: {e}") diff --git a/handlers/start.py b/handlers/start.py index a48f02d6..cf7bec02 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -101,14 +101,7 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin await message.answer("❌ Вы не можете получить подарок от самого себя.") return await show_start_menu(message, admin, session) - await session.execute( - """ - INSERT INTO connections (tg_id, balance, trial) - VALUES ($1, 0, 1) - ON CONFLICT (tg_id) DO UPDATE SET trial = 1 - """, - recipient_tg_id, - ) + await add_connection(tg_id=recipient_tg_id, session=session) selected_months = gift_info["selected_months"] expiry_time = gift_info["expiry_time"] From 4dcfa313312b15cd1583852a6579b42749f209fd Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 23 Jan 2025 08:03:58 +0300 Subject: [PATCH 22/49] Add server management functionality by introducing `add_server_to_db` function for streamlined database interactions. Refactor server addition logic in `sync_servers_with_db` and admin handlers to utilize the new function, enhancing code clarity and session management. --- database.py | 35 +++++++++++++++++++++++++++++++++ handlers/admin/admin_servers.py | 24 ++++++++++------------ servers.py | 19 ++++++++---------- 3 files changed, 53 insertions(+), 25 deletions(-) diff --git a/database.py b/database.py index 8dba5f4a..0b21d734 100644 --- a/database.py +++ b/database.py @@ -1295,3 +1295,38 @@ async def delete_key(identifier, session): logger.info(f"Ключ с идентификатором {identifier} успешно удалён") except Exception as e: logger.error(f"Ошибка при удалении ключа с идентификатором {identifier} из базы данных: {e}") + + +async def add_server_to_db( + cluster_name: str, server_name: str, api_url: str, subscription_url: str, inbound_id: int, session: Any +): + """ + Добавляет новый сервер в базу данных. + + Args: + cluster_name (str): Название кластера + server_name (str): Название сервера + api_url (str): URL API сервера + subscription_url (str): URL подписки + inbound_id (int): ID входящего подключения + session (Any): Сессия базы данных + + Raises: + Exception: В случае ошибки при добавлении сервера + """ + try: + await session.execute( + """ + INSERT INTO servers (cluster_name, server_name, api_url, subscription_url, inbound_id) + VALUES ($1, $2, $3, $4, $5) + """, + cluster_name, + server_name, + api_url, + subscription_url, + inbound_id, + ) + logger.info(f"Сервер {server_name} успешно добавлен в кластер {cluster_name}") + except Exception as e: + logger.error(f"Ошибка при добавлении сервера {server_name} в кластер {cluster_name}: {e}") + raise diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index 1a039c77..2e8aefc4 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -1,4 +1,5 @@ import asyncio +from typing import Any import asyncpg from aiogram import F, Router, types @@ -10,7 +11,7 @@ from py3xui import AsyncApi from backup import create_backup_and_send_to_admins from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL -from database import check_unique_server_name, get_servers_from_db +from database import add_server_to_db, check_unique_server_name, get_servers_from_db from filters.admin import IsAdminFilter from handlers.keys.key_utils import create_key_on_cluster from logger import logger @@ -211,7 +212,7 @@ async def handle_subscription_url_input(message: types.Message, state: FSMContex @router.message(UserEditorState.waiting_for_inbound_id, IsAdminFilter()) -async def handle_inbound_id_input(message: types.Message, state: FSMContext): +async def handle_inbound_id_input(message: types.Message, state: FSMContext, session: Any): inbound_id = message.text.strip() if not inbound_id.isdigit(): @@ -224,19 +225,14 @@ async def handle_inbound_id_input(message: types.Message, state: FSMContext): api_url = user_data.get("api_url") subscription_url = user_data.get("subscription_url") - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( - """ - INSERT INTO servers (cluster_name, server_name, api_url, subscription_url, inbound_id) - VALUES ($1, $2, $3, $4, $5) - """, - cluster_name, - server_name, - api_url, - subscription_url, - inbound_id, + await add_server_to_db( + cluster_name=cluster_name, + server_name=server_name, + api_url=api_url, + subscription_url=subscription_url, + inbound_id=inbound_id, + session=session, ) - await conn.close() builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад к кластерам", callback_data="servers_editor")) diff --git a/servers.py b/servers.py index 4a2b0b81..af439a0b 100644 --- a/servers.py +++ b/servers.py @@ -9,7 +9,7 @@ from ping3 import ping from bot import bot from config import ADMIN_ID, DATABASE_URL, PING_TIME -from database import get_servers_from_db +from database import add_server_to_db, get_servers_from_db from logger import logger try: @@ -44,16 +44,13 @@ async def sync_servers_with_db(): ) if not exists: - await conn.execute( - """ - INSERT INTO servers (cluster_name, server_name, api_url, subscription_url, inbound_id) - VALUES ($1, $2, $3, $4, $5) - """, - cluster_name, - server_info["name"], - server_info["API_URL"], - server_info["SUBSCRIPTION"], - server_info["INBOUND_ID"], + await add_server_to_db( + cluster_name=cluster_name, + server_name=server_info["name"], + api_url=server_info["API_URL"], + subscription_url=server_info["SUBSCRIPTION"], + inbound_id=server_info["INBOUND_ID"], + session=conn, ) logger.info(f"Сервер {server_info['name']} из кластера {cluster_name} добавлен в базу данных.") else: From 17d519d44016690806df7012f45b4e9dce7c9f8e Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 23 Jan 2025 08:41:44 +0300 Subject: [PATCH 23/49] Refactor key retrieval and management logic across multiple handlers. Update `get_key_details` to include email in the fetched record and replace direct SQL queries with dedicated functions for improved code clarity and maintainability. Add a TODO comment in `admin_servers.py` for future enhancements. --- database.py | 3 ++- handlers/admin/admin_servers.py | 1 + handlers/admin/admin_user_editor.py | 9 +++++---- handlers/keys/key_management.py | 6 +----- handlers/keys/keys.py | 25 ++----------------------- 5 files changed, 11 insertions(+), 33 deletions(-) diff --git a/database.py b/database.py index 0b21d734..ec8261d6 100644 --- a/database.py +++ b/database.py @@ -1234,7 +1234,7 @@ async def store_gift_link( async def get_key_details(email, session): record = await session.fetchrow( """ - SELECT k.key, k.expiry_time, k.server_id,k, k.client_id, k.created_at, c.tg_id, c.balance + SELECT k.key, k.email, k.expiry_time, k.server_id,k, k.client_id, k.created_at, c.tg_id, c.balance FROM keys k JOIN connections c ON k.tg_id = c.tg_id WHERE k.email = $1 @@ -1270,6 +1270,7 @@ async def get_key_details(email, session): "server_name": cluster_name, "balance": record["balance"], "tg_id": record["tg_id"], + "email": record["email"], } diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index 2e8aefc4..1fd5dcd9 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -300,6 +300,7 @@ async def sync_cluster_handler(callback_query: types.CallbackQuery): conn = await asyncpg.connect(DATABASE_URL) try: + #TODO query_keys = """ SELECT tg_id, client_id, email, expiry_time FROM keys diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index 64c7b6e1..58af54ab 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -14,6 +14,7 @@ from database import ( delete_user_data, get_client_id_by_email, get_key_details, + get_keys, get_servers_from_db, restore_trial, update_key_expiry, @@ -74,7 +75,7 @@ async def handle_username_input(message: types.Message, state: FSMContext, sessi tg_id = user_record["tg_id"] username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id) balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) - key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) + key_records = await get_keys(tg_id, session) referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) if balance is None: @@ -154,7 +155,7 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext, session: tg_id = int(message.text) username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id) balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) - key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) + key_records = await get_keys(tg_id, session) referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) if balance is None: @@ -398,7 +399,7 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se await state.clear() return - record = await session.fetchrow("SELECT server_id FROM keys WHERE client_id = $1", client_id) + record = await get_key_details(email, session) if not record: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) @@ -512,7 +513,7 @@ async def handle_user_info(callback_query: types.CallbackQuery, state: FSMContex tg_id = int(callback_query.data.split("|")[1]) username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id) balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) - key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) + key_records = await get_keys(tg_id, session) referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) builder = InlineKeyboardBuilder() diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index a06f004f..70c775ec 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -228,11 +228,7 @@ async def create_key( key_name = generate_random_email() logger.info(f"[Key Generation] Сгенерировано имя ключа: {key_name} для пользователя {tg_id}") - existing_key = await session.fetchrow( - "SELECT * FROM keys WHERE email = $1 AND tg_id = $2", - key_name, - tg_id, - ) + existing_key = await get_key_details(key_name, session) if not existing_key: break logger.warning(f"[Key Generation] Имя ключа {key_name} уже существует. Генерация нового.") diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 20c036fb..af4e6b56 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -264,20 +264,7 @@ async def process_callback_update_subscription(callback_query: types.CallbackQue client_id = record["client_id"] public_link = f"{PUBLIC_LINK}{email}/{tg_id}" - try: - await session.execute( - """ - DELETE FROM keys - WHERE tg_id = $1 AND email = $2 - """, - tg_id, - email, - ) - except Exception as delete_error: - await callback_query.message.answer( - f"Ошибка при удалении старой подписки: {delete_error}", - ) - return + await delete_key(client_id, session) least_loaded_cluster_id = await get_least_loaded_cluster() @@ -517,15 +504,7 @@ async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_g await bot.send_message(tg_id, response_message, reply_markup=builder.as_markup()) conn = await asyncpg.connect(DATABASE_URL) - key_info = await conn.fetchrow( - """ - SELECT server_id - FROM keys - WHERE tg_id = $1 AND client_id = $2 - """, - tg_id, - client_id, - ) + key_info = await get_key_details(email, conn) if not key_info: logger.error(f"[RENEW] Ключ с client_id {client_id} для пользователя {tg_id} не найден.") From 533e1283eab80699209def3bea13077d5afec1f6 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 23 Jan 2025 05:42:01 +0000 Subject: [PATCH 24/49] Auto-format code with Ruff using pyproject.toml --- handlers/admin/admin_servers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index 1fd5dcd9..151475a8 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -300,7 +300,7 @@ async def sync_cluster_handler(callback_query: types.CallbackQuery): conn = await asyncpg.connect(DATABASE_URL) try: - #TODO + # TODO query_keys = """ SELECT tg_id, client_id, email, expiry_time FROM keys From 70e9bfc2ca4e124c61fae83b734ebff7efd56b8c Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 23 Jan 2025 08:49:03 +0300 Subject: [PATCH 25/49] Refactor client management functions in client.py to use a dataclass for client configuration. Update add_client, extend_client_key, and delete_client functions to improve parameter handling and logging. Enhance error handling and documentation for better clarity and maintainability. --- client.py | 136 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 87 insertions(+), 49 deletions(-) diff --git a/client.py b/client.py index f0561bc3..e1995d53 100644 --- a/client.py +++ b/client.py @@ -1,76 +1,105 @@ +from dataclasses import dataclass +from typing import Optional, Dict, Any, List import py3xui from config import LIMIT_IP, SUPERNODE from logger import logger -async def add_client( - xui, - client_id: str, - email: str, - tg_id: str, - limit_ip: int, - total_gb: int, - expiry_time: int, - enable: bool, - flow: str, - inbound_id: int, - sub_id, -): +@dataclass +class ClientConfig: + """Конфигурация клиента для добавления/обновления.""" + client_id: str + email: str + tg_id: str + limit_ip: int + total_gb: int + expiry_time: int + enable: bool + flow: str + inbound_id: int + sub_id: str + + +async def add_client(xui: py3xui.API, config: ClientConfig) -> Dict[str, Any]: """ - Adds a client to the server via 3x-ui. + Добавляет клиента на сервер через 3x-ui. + + Args: + xui: Экземпляр API клиента + config: Конфигурация клиента + + Returns: + Dict[str, Any]: Результат операции в формате + {'status': 'success'|'failed'|'duplicate', 'error': str, 'email': str} """ try: await xui.login() client = py3xui.Client( - id=client_id, - email=email.lower(), - limit_ip=limit_ip, - total_gb=total_gb, - expiry_time=expiry_time, - enable=enable, - tg_id=tg_id, - sub_id=sub_id, - flow=flow, + id=config.client_id, + email=config.email.lower(), + limit_ip=config.limit_ip, + total_gb=config.total_gb, + expiry_time=config.expiry_time, + enable=config.enable, + tg_id=config.tg_id, + sub_id=config.sub_id, + flow=config.flow, ) - response = await xui.client.add(inbound_id, [client]) - - logger.info(f"Клиент {email} успешно добавлен с ID {client_id}.") + response = await xui.client.add(config.inbound_id, [client]) + logger.info(f"Клиент {config.email} успешно добавлен с ID {config.client_id}") return response if response else {"status": "failed"} except Exception as e: error_message = str(e) - if "Duplicate email" in error_message: - logger.warning(f"Дублированный email: {email}. Пропуск. Сообщение: {error_message}") - return {"status": "duplicate", "email": email} + logger.warning(f"Дублированный email: {config.email}. Пропуск. Сообщение: {error_message}") + return {"status": "duplicate", "email": config.email} - logger.error(f"Ошибка при добавлении клиента {email}: {error_message}") + logger.error(f"Ошибка при добавлении клиента {config.email}: {error_message}") return {"status": "failed", "error": error_message} async def extend_client_key( - xui, inbound_id, email: str, new_expiry_time: int, client_id: str, total_gb: int, sub_id=str -): + xui: py3xui.API, + inbound_id: int, + email: str, + new_expiry_time: int, + client_id: str, + total_gb: int, + sub_id: str +) -> Optional[bool]: """ - Функция для обновления срока действия ключа клиента по email. + Обновляет срок действия ключа клиента. + + Args: + xui: Экземпляр API клиента + inbound_id: ID входящего соединения + email: Email клиента + new_expiry_time: Новое время истечения + client_id: ID клиента + total_gb: Общий объем трафика + sub_id: ID подписки + + Returns: + Optional[bool]: True если успешно, False если ошибка, None если клиент не найден """ - await xui.login() try: + await xui.login() client = await xui.client.get_by_email(email) if not client: - logger.warning(f"Клиент с email {email} не найден.") - return + logger.warning(f"Клиент с email {email} не найден") + return None if not client.id: - logger.warning(f"Ошибка: клиент {email} не имеет действительного ID.") - return + logger.warning(f"Ошибка: клиент {email} не имеет действительного ID") + return None - logger.info(f"Обновление ключа клиента {client.email} с ID {client.id} до нового времени: {new_expiry_time}") + logger.info(f"Обновление ключа клиента {email} с ID {client.id} до {new_expiry_time}") client.id = client_id client.expiry_time = new_expiry_time @@ -83,39 +112,48 @@ async def extend_client_key( await xui.client.update(client.id, client) await xui.client.reset_stats(inbound_id, email) - logger.info(f"Ключ клиента {client.email} успешно продлён до {new_expiry_time}.") + logger.info(f"Ключ клиента {email} успешно продлён до {new_expiry_time}") + return True except Exception as e: logger.error(f"Ошибка при обновлении клиента с email {email}: {e}") + return False async def delete_client( - xui, + xui: py3xui.API, inbound_id: int, email: str, client_id: str, ) -> bool: """ - Функция для удаления клиента с сервера 3x-ui. - Возвращает True при успешном удалении, иначе False. + Удаляет клиента с сервера 3x-ui. + + Args: + xui: Экземпляр API клиента + inbound_id: ID входящего соединения + email: Email клиента + client_id: ID клиента + + Returns: + bool: True если удаление успешно, False в противном случае """ - await xui.login() try: + await xui.login() + if SUPERNODE: await xui.client.delete(inbound_id, client_id) - logger.info(f"Клиент с ID {client_id} был удален успешно (SUPERNODE).") + logger.info(f"Клиент с ID {client_id} был удален успешно (SUPERNODE)") return True client = await xui.client.get_by_email(email) - if not client: - logger.warning(f"Клиент с email {email} и ID {client_id} не найден.") + logger.warning(f"Клиент с email {email} и ID {client_id} не найден") return False client.id = client_id - await xui.client.delete(inbound_id, client.id) - logger.info(f"Клиент с ID {client_id} был удален успешно.") + logger.info(f"Клиент с ID {client_id} был удален успешно") return True except Exception as e: From c8506987032f39897abdd87b50e3d969060bc6f3 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 23 Jan 2025 08:50:04 +0300 Subject: [PATCH 26/49] Refactor client configuration and improve code clarity in client.py and admin_servers.py. Added a new line in ClientConfig for better readability and updated the extend_client_key function signature for consistency. Corrected a TODO comment format in admin_servers.py for improved code standards. --- client.py | 9 ++------- handlers/admin/admin_servers.py | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/client.py b/client.py index e1995d53..8c4d452c 100644 --- a/client.py +++ b/client.py @@ -9,6 +9,7 @@ from logger import logger @dataclass class ClientConfig: """Конфигурация клиента для добавления/обновления.""" + client_id: str email: str tg_id: str @@ -64,13 +65,7 @@ async def add_client(xui: py3xui.API, config: ClientConfig) -> Dict[str, Any]: async def extend_client_key( - xui: py3xui.API, - inbound_id: int, - email: str, - new_expiry_time: int, - client_id: str, - total_gb: int, - sub_id: str + xui: py3xui.API, inbound_id: int, email: str, new_expiry_time: int, client_id: str, total_gb: int, sub_id: str ) -> Optional[bool]: """ Обновляет срок действия ключа клиента. diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index 1fd5dcd9..151475a8 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -300,7 +300,7 @@ async def sync_cluster_handler(callback_query: types.CallbackQuery): conn = await asyncpg.connect(DATABASE_URL) try: - #TODO + # TODO query_keys = """ SELECT tg_id, client_id, email, expiry_time FROM keys From 31dd3d6654d21b330a54cfe5b4d05d68b41b209c Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 23 Jan 2025 10:24:16 +0300 Subject: [PATCH 27/49] Refactor client management in key_utils.py and trial_key.py to utilize ClientConfig dataclass for improved parameter handling. This change enhances code clarity and maintainability by consolidating client configuration parameters into a single structured object, streamlining the add_client function calls across multiple handlers. --- handlers/keys/key_utils.py | 46 +++++++++++++++++++++----------------- handlers/keys/trial_key.py | 24 +++++++++++--------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index 3657f77f..98877801 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -2,7 +2,7 @@ import asyncio from py3xui import AsyncApi -from client import add_client, delete_client, extend_client_key +from client import ClientConfig, add_client, delete_client, extend_client_key from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, SUPERNODE, TOTAL_GB from database import get_servers_from_db from logger import logger @@ -85,16 +85,18 @@ async def create_client_on_server( await add_client( xui, - client_id, - unique_email, - tg_id, - limit_ip=LIMIT_IP, - total_gb=TOTAL_GB, - expiry_time=expiry_timestamp, - enable=True, - flow="xtls-rprx-vision", - inbound_id=int(inbound_id), - sub_id=sub_id, + ClientConfig( + client_id=client_id, + email=unique_email, + tg_id=tg_id, + limit_ip=LIMIT_IP, + total_gb=TOTAL_GB, + expiry_time=expiry_timestamp, + enable=True, + flow="xtls-rprx-vision", + inbound_id=int(inbound_id), + sub_id=sub_id, + ), ) if SUPERNODE: @@ -208,16 +210,18 @@ async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id tasks.append( add_client( xui, - client_id, - email, - tg_id, - limit_ip=LIMIT_IP, - total_gb=TOTAL_GB, - expiry_time=expiry_time, - enable=True, - flow="xtls-rprx-vision", - inbound_id=int(inbound_id), - sub_id=email, + ClientConfig( + client_id=client_id, + email=email, + tg_id=tg_id, + limit_ip=LIMIT_IP, + total_gb=TOTAL_GB, + expiry_time=expiry_time, + enable=True, + flow="xtls-rprx-vision", + inbound_id=int(inbound_id), + sub_id=email, + ), ) ) diff --git a/handlers/keys/trial_key.py b/handlers/keys/trial_key.py index 00048577..c54b31a5 100644 --- a/handlers/keys/trial_key.py +++ b/handlers/keys/trial_key.py @@ -6,7 +6,7 @@ from typing import Any import pytz from py3xui import AsyncApi -from client import add_client +from client import ClientConfig, add_client from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, TRIAL_TIME from database import get_servers_from_db, get_trial, store_key, use_trial from handlers.texts import INSTRUCTIONS @@ -56,16 +56,18 @@ async def create_trial_key(tg_id: int, session: Any): username=ADMIN_USERNAME, password=ADMIN_PASSWORD, ), - client_id, - email, - tg_id, - limit_ip=LIMIT_IP, - total_gb=TOTAL_GB, - expiry_time=expiry_timestamp, - enable=True, - flow="xtls-rprx-vision", - inbound_id=int(server_info["inbound_id"]), - sub_id=base_email, + ClientConfig( + client_id=client_id, + email=email, + tg_id=tg_id, + limit_ip=LIMIT_IP, + total_gb=TOTAL_GB, + expiry_time=expiry_timestamp, + enable=True, + flow="xtls-rprx-vision", + inbound_id=int(server_info["inbound_id"]), + sub_id=base_email, + ), ) ) From e42a66dde7397a709d0216ba015998cbf663cff9 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 23 Jan 2025 10:38:32 +0300 Subject: [PATCH 28/49] Refactor database interaction functions in database.py and update related handlers. Renamed functions for clarity: `save_temporary_data` to `create_temporary_data`, `add_blocked_user` to `create_blocked_user`, `remove_blocked_user` to `delete_blocked_user`, and `delete_coupon_from_db` to `delete_coupon`. Updated trial management function from `restore_trial` to `set_trial` to better reflect its purpose. Adjusted corresponding calls in notifications, start, user, and admin handlers for consistency and improved readability. --- database.py | 56 +++++++++-------------------- handlers/admin/admin_coupons.py | 4 +-- handlers/admin/admin_panel.py | 4 +-- handlers/admin/admin_user_editor.py | 4 +-- handlers/keys/key_management.py | 4 +-- handlers/keys/keys.py | 4 +-- handlers/keys/trial_key.py | 4 +-- handlers/notifications.py | 4 +-- handlers/start.py | 4 +-- handlers/user.py | 6 ++-- 10 files changed, 35 insertions(+), 59 deletions(-) diff --git a/database.py b/database.py index ec8261d6..83e528c3 100644 --- a/database.py +++ b/database.py @@ -9,7 +9,7 @@ from config import DATABASE_URL, REFERRAL_BONUS_PERCENTAGES from logger import logger -async def save_temporary_data(session, tg_id: int, state: str, data: dict): +async def create_temporary_data(session, tg_id: int, state: str, data: dict): """Сохраняет временные данные пользователя.""" await session.execute( """ @@ -34,17 +34,18 @@ async def get_temporary_data(session, tg_id: int) -> dict | None: async def clear_temporary_data(session, tg_id: int): + # TODO rename delete_temporary_data await session.execute("DELETE FROM temporary_data WHERE tg_id = $1", tg_id) -async def add_blocked_user(tg_id: int, conn: asyncpg.Connection): +async def create_blocked_user(tg_id: int, conn: asyncpg.Connection): await conn.execute( "INSERT INTO blocked_users (tg_id) VALUES ($1) ON CONFLICT (tg_id) DO NOTHING", tg_id, ) -async def remove_blocked_user(tg_id: int | list[int], conn: asyncpg.Connection): +async def delete_blocked_user(tg_id: int | list[int], conn: asyncpg.Connection): """ Удаляет пользователя или список пользователей из списка заблокированных. @@ -166,7 +167,7 @@ async def get_all_coupons(session: Any, page: int = 1, per_page: int = 10): return {"coupons": [], "total": 0, "pages": 0, "current_page": page} -async def delete_coupon_from_db(coupon_code: str, session: Any): +async def delete_coupon(coupon_code: str, session: Any): """ Удаляет купон из базы данных по его коду. @@ -181,7 +182,7 @@ async def delete_coupon_from_db(coupon_code: str, session: Any): Exception: В случае ошибки при выполнении запроса к базе данных Example: - result = await delete_coupon_from_db('SALE50', session) + result = await delete_coupon('SALE50', session) """ try: coupon_record = await session.fetchrow( @@ -210,59 +211,34 @@ async def delete_coupon_from_db(coupon_code: str, session: Any): return False -async def restore_trial(tg_id: int, session: Any): +async def set_trial(tg_id: int, status: int, session: Any): """ - Восстанавливает возможность использования триального периода для пользователя. + Устанавливает статус триального периода для пользователя. Args: tg_id (int): Telegram ID пользователя + status (int): Статус триального периода (0 - доступен, 1 - использован) session (Any): Сессия базы данных Returns: - bool: True, если триал успешно восстановлен, False в случае ошибки + bool: True, если статус успешно установлен, False в случае ошибки """ try: await session.execute( """ INSERT INTO connections (tg_id, trial) - VALUES ($1, 0) + VALUES ($1, $2) ON CONFLICT (tg_id) - DO UPDATE SET trial = 0 + DO UPDATE SET trial = $2 """, tg_id, + status, ) - logger.info(f"Триальный период успешно восстановлен для пользователя {tg_id}") + status_text = "восстановлен" if status == 0 else "использован" + logger.info(f"Триальный период успешно {status_text} для пользователя {tg_id}") return True except Exception as e: - logger.error(f"Ошибка при восстановлении триального периода для пользователя {tg_id}: {e}") - return False - - -async def use_trial(tg_id: int, session: Any): - """ - Отмечает использование триального периода для пользователя. - - Args: - tg_id (int): Telegram ID пользователя - session (Any): Сессия базы данных - - Returns: - bool: True, если триал успешно использован, False в случае ошибки - """ - try: - await session.execute( - """ - INSERT INTO connections (tg_id, trial) - VALUES ($1, 1) - ON CONFLICT (tg_id) - DO UPDATE SET trial = 1 - """, - tg_id, - ) - logger.info(f"Триальный период успешно использован для пользователя {tg_id}") - return True - except Exception as e: - logger.error(f"Ошибка при использовании триала для пользователя {tg_id}: {e}") + logger.error(f"Ошибка при установке статуса триального периода для пользователя {tg_id}: {e}") return False diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index e83c8fcf..78b9ba9f 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -6,7 +6,7 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from database import create_coupon, delete_coupon_from_db, get_all_coupons +from database import create_coupon, delete_coupon, get_all_coupons from filters.admin import IsAdminFilter from logger import logger @@ -81,7 +81,7 @@ async def handle_delete_coupon(callback_query: types.CallbackQuery, session: Any coupon_code = callback_query.data[len("delete_coupon_") :] try: - result = await delete_coupon_from_db(coupon_code, session) + result = await delete_coupon(coupon_code, session) if result: await show_coupon_list(callback_query, session) diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index 81d52d52..29af84ea 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -12,7 +12,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from backup import backup_database from bot import bot -from database import delete_user_data, remove_blocked_user +from database import delete_user_data, delete_blocked_user from filters.admin import IsAdminFilter from logger import logger @@ -488,7 +488,7 @@ async def delete_banned_users(callback_query: types.CallbackQuery, session: Any) for tg_id in blocked_ids: await delete_user_data(session, tg_id) - await remove_blocked_user(blocked_ids, session) + await delete_blocked_user(blocked_ids, session) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management")) diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index 58af54ab..9a577846 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -16,7 +16,7 @@ from database import ( get_key_details, get_keys, get_servers_from_db, - restore_trial, + set_trial, update_key_expiry, ) from filters.admin import IsAdminFilter @@ -207,7 +207,7 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext, session: async def handle_restore_trial(callback_query: types.CallbackQuery, session: Any): tg_id = int(callback_query.data.split("_")[2]) - await restore_trial(tg_id, session) + await set_trial(tg_id, 0, session) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад в меню администратора", callback_data="admin")) diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 70c775ec..458ac4c9 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -27,7 +27,7 @@ from database import ( get_balance, get_key_details, get_trial, - save_temporary_data, + create_temporary_data, store_key, update_balance, ) @@ -129,7 +129,7 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any): if balance < plan_price: required_amount = plan_price - balance - await save_temporary_data( + await create_temporary_data( session, tg_id, "waiting_for_payment", diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index af4e6b56..ecbb1171 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -31,7 +31,7 @@ from database import ( get_balance, get_key_details, get_servers_from_db, - save_temporary_data, + create_temporary_data, store_key, update_balance, update_key_expiry, @@ -449,7 +449,7 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery, sessi f"[RENEW] Пользователю {tg_id} не хватает {required_amount}₽. Запуск доплаты через {USE_NEW_PAYMENT_FLOW}" ) - await save_temporary_data( + await create_temporary_data( session, tg_id, "waiting_for_renewal_payment", diff --git a/handlers/keys/trial_key.py b/handlers/keys/trial_key.py index c54b31a5..12725c02 100644 --- a/handlers/keys/trial_key.py +++ b/handlers/keys/trial_key.py @@ -8,7 +8,7 @@ from py3xui import AsyncApi from client import ClientConfig, add_client from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, TRIAL_TIME -from database import get_servers_from_db, get_trial, store_key, use_trial +from database import get_servers_from_db, get_trial, store_key, set_trial from handlers.texts import INSTRUCTIONS from handlers.utils import generate_random_email, get_least_loaded_cluster from logger import logger @@ -83,5 +83,5 @@ async def create_trial_key(tg_id: int, session: Any): session=session, ) - await use_trial(tg_id, session) + await set_trial(tg_id, 1, session) return result diff --git a/handlers/notifications.py b/handlers/notifications.py index 369e8e48..67cd9890 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -23,7 +23,7 @@ from config import ( TRIAL_TIME, ) from database import ( - add_blocked_user, + create_blocked_user, add_notification, check_notification_time, delete_key, @@ -356,7 +356,7 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): except TelegramForbiddenError: logger.warning(f"Бот заблокирован пользователем {tg_id}. Добавляем в blocked_users.") - await add_blocked_user(tg_id, conn) + await create_blocked_user(tg_id, conn) except Exception as e: logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}") diff --git a/handlers/start.py b/handlers/start.py index cf7bec02..25b83bfd 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -29,7 +29,7 @@ from database import ( add_referral, check_connection_exists, get_trial, - use_trial, + set_trial, ) from handlers.buttons.add_subscribe import ( DOWNLOAD_ANDROID_BUTTON, @@ -216,7 +216,7 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session: Any): if "error" in trial_key_info: await callback_query.message.answer(trial_key_info["error"]) else: - await use_trial(user_id, session) + await set_trial(user_id, 1, session) key_message = ( f"🔑 Ваш персональный ключ доступа:\n" diff --git a/handlers/user.py b/handlers/user.py index bc5e590d..44a190ec 100644 --- a/handlers/user.py +++ b/handlers/user.py @@ -4,7 +4,7 @@ from aiogram import Router from aiogram.filters.chat_member_updated import KICKED, MEMBER, ChatMemberUpdatedFilter from aiogram.types import ChatMemberUpdated -from database import add_blocked_user, remove_blocked_user +from database import create_blocked_user, delete_blocked_user from logger import logger router = Router() @@ -13,10 +13,10 @@ router = Router() @router.my_chat_member(ChatMemberUpdatedFilter(member_status_changed=KICKED)) async def user_blocked_bot(event: ChatMemberUpdated, session: Any): logger.info(f"User {event.from_user.id} blocked the bot.") - await add_blocked_user(event.from_user.id, session) + await create_blocked_user(event.from_user.id, session) @router.my_chat_member(ChatMemberUpdatedFilter(member_status_changed=MEMBER)) async def user_unblocked_bot(event: ChatMemberUpdated, session: Any): logger.info(f"User {event.from_user.id} unblocked the bot.") - await remove_blocked_user(event.from_user.id, session) + await delete_blocked_user(event.from_user.id, session) From 906601f5cdc504cc1a7cf01d7af123e5b7f0d5fb Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 23 Jan 2025 11:06:25 +0300 Subject: [PATCH 29/49] Refactor database interaction in key management functions to utilize session objects for improved performance and consistency. Updated `get_keys`, `get_keys_by_server`, `update_balance`, and `update_key_expiry` functions to accept a session parameter, allowing for better session management across handlers. Adjusted related handler functions to pass the session, enhancing code clarity and maintainability. --- database.py | 130 +++++++++------------------- handlers/admin/admin_servers.py | 15 +--- handlers/admin/admin_user_editor.py | 2 +- handlers/coupons.py | 2 +- handlers/keys/key_management.py | 2 +- handlers/keys/keys.py | 10 +-- handlers/notifications.py | 12 +-- 7 files changed, 57 insertions(+), 116 deletions(-) diff --git a/database.py b/database.py index 83e528c3..688f0ebf 100644 --- a/database.py +++ b/database.py @@ -349,7 +349,7 @@ async def store_key( raise -async def get_keys(tg_id: int): +async def get_keys(tg_id: int, session: Any): """ Получает список ключей для указанного пользователя. @@ -362,10 +362,8 @@ async def get_keys(tg_id: int): Raises: Exception: В случае ошибки при подключении к базе данных или выполнении запроса """ - conn = None try: - conn = await asyncpg.connect(DATABASE_URL) - records = await conn.fetch( + records = await session.fetch( """ SELECT client_id, email, created_at, key FROM keys @@ -378,17 +376,14 @@ async def get_keys(tg_id: int): except Exception as e: logger.error(f"Ошибка при получении ключей для пользователя {tg_id}: {e}") raise - finally: - if conn: - await conn.close() -async def get_keys_by_server(tg_id: int, server_id: str): +async def get_keys_by_server(tg_id: int | None, server_id: str, session: Any): """ - Получает список ключей для указанного пользователя на определенном сервере. + Получает список ключей на определенном сервере. Если tg_id=None, возвращает все ключи на сервере. Args: - tg_id (int): Telegram ID пользователя + tg_id (int | None): Telegram ID пользователя или None для всех пользователей server_id (str): Идентификатор сервера Returns: @@ -397,53 +392,36 @@ async def get_keys_by_server(tg_id: int, server_id: str): Raises: Exception: В случае ошибки при подключении к базе данных или выполнении запроса """ - conn = None try: - conn = await asyncpg.connect(DATABASE_URL) - records = await conn.fetch( - """ - SELECT client_id, email, created_at, key - FROM keys - WHERE tg_id = $1 AND server_id = $2 - """, - tg_id, - server_id, - ) - logger.info(f"Успешно получено {len(records)} ключей для пользователя {tg_id} на сервере {server_id}") + if tg_id is not None: + records = await session.fetch( + """ + SELECT * + FROM keys + WHERE tg_id = $1 AND server_id = $2 + """, + tg_id, + server_id, + ) + logger.info(f"Успешно получено {len(records)} ключей для пользователя {tg_id} на сервере {server_id}") + else: + records = await session.fetch( + """ + SELECT * + FROM keys + WHERE server_id = $1 + """, + server_id, + ) + logger.info(f"Успешно получено {len(records)} ключей на сервере {server_id}") + return records except Exception as e: - logger.error(f"Ошибка при получении ключей для пользователя {tg_id} на сервере {server_id}: {e}") + error_msg = f"Ошибка при получении ключей на сервере {server_id}" + if tg_id is not None: + error_msg += f" для пользователя {tg_id}" + logger.error(f"{error_msg}: {e}") raise - finally: - if conn: - await conn.close() - - -async def has_active_key(tg_id: int) -> bool: - """ - Проверяет наличие активных ключей для указанного пользователя. - - Args: - tg_id (int): Telegram ID пользователя - - Returns: - bool: True, если у пользователя есть активные ключи, иначе False - - Raises: - Exception: В случае ошибки при подключении к базе данных или выполнении запроса - """ - conn = None - try: - conn = await asyncpg.connect(DATABASE_URL) - count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE tg_id = $1", tg_id) - logger.info(f"Проверка наличия ключей для пользователя {tg_id}. Найдено ключей: {count}") - return count > 0 - except Exception as e: - logger.error(f"Ошибка при проверке наличия ключей для пользователя {tg_id}: {e}") - raise - finally: - if conn: - await conn.close() async def get_balance(tg_id: int) -> float: @@ -473,21 +451,25 @@ async def get_balance(tg_id: int) -> float: await conn.close() -async def update_balance(tg_id: int, amount: float): +async def update_balance(tg_id: int, amount: float, session: Any = None): """ Обновляет баланс пользователя в базе данных. Args: tg_id (int): Telegram ID пользователя amount (float): Сумма для обновления баланса + session (Any, optional): Сессия базы данных. Если не передана, создается новая. Raises: Exception: В случае ошибки при подключении к базе данных или обновлении баланса """ conn = None try: - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( + if session is None: + conn = await asyncpg.connect(DATABASE_URL) + session = conn + + await session.execute( """ UPDATE connections SET balance = balance + $1 @@ -504,7 +486,7 @@ async def update_balance(tg_id: int, amount: float): logger.error(f"Ошибка при обновлении баланса для пользователя {tg_id}: {e}") raise finally: - if conn: + if conn is not None: await conn.close() @@ -555,28 +537,6 @@ async def get_key_count(tg_id: int) -> int: await conn.close() -async def get_all_users(conn): - """ - Получает список всех пользователей из базы данных. - - Args: - conn: Подключение к базе данных - - Returns: - list: Список Telegram ID всех пользователей - - Raises: - Exception: В случае ошибки при получении данных - """ - try: - users = await conn.fetch("SELECT tg_id FROM connections") - logger.info(f"Получен список всех пользователей. Количество: {len(users)}") - return users - except Exception as e: - logger.error(f"Ошибка при получении списка пользователей: {e}") - raise - - async def add_referral(referred_tg_id: int, referrer_tg_id: int, session: Any): try: if referred_tg_id == referrer_tg_id: @@ -784,7 +744,7 @@ async def get_referral_stats(referrer_tg_id: int): logger.info("Закрытие подключения к базе данных") -async def update_key_expiry(client_id: str, new_expiry_time: int): +async def update_key_expiry(client_id: str, new_expiry_time: int, session: Any): """ Обновление времени истечения ключа для указанного клиента. @@ -795,12 +755,8 @@ async def update_key_expiry(client_id: str, new_expiry_time: int): Raises: Exception: В случае ошибки при подключении к базе данных или обновлении ключа """ - conn = None try: - conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для обновления времени истечения ключа клиента {client_id}") - - await conn.execute( + await session.execute( """ UPDATE keys SET expiry_time = $1, notified = FALSE, notified_24h = FALSE @@ -814,10 +770,6 @@ async def update_key_expiry(client_id: str, new_expiry_time: int): except Exception as e: logger.error(f"Ошибка при обновлении времени истечения ключа для клиента {client_id}: {e}") raise - finally: - if conn: - await conn.close() - logger.info("Закрытие подключения к базе данных") async def add_balance_to_client(client_id: str, amount: float): diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index 151475a8..c110fda7 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -11,7 +11,7 @@ from py3xui import AsyncApi from backup import create_backup_and_send_to_admins from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL -from database import add_server_to_db, check_unique_server_name, get_servers_from_db +from database import add_server_to_db, check_unique_server_name, get_keys_by_server, get_servers_from_db from filters.admin import IsAdminFilter from handlers.keys.key_utils import create_key_on_cluster from logger import logger @@ -294,19 +294,12 @@ async def handle_manage_cluster(callback_query: types.CallbackQuery, state: FSMC @router.callback_query(F.data.startswith("sync_cluster|"), IsAdminFilter()) -async def sync_cluster_handler(callback_query: types.CallbackQuery): +async def sync_cluster_handler(callback_query: types.CallbackQuery, session: Any): """Обработчик для синхронизации ключей на всех серверах выбранного кластера.""" cluster_name = callback_query.data.split("|")[1] - conn = await asyncpg.connect(DATABASE_URL) try: - # TODO - query_keys = """ - SELECT tg_id, client_id, email, expiry_time - FROM keys - WHERE server_id = $1 - """ - keys_to_sync = await conn.fetch(query_keys, cluster_name) + keys_to_sync = await get_keys_by_server(None, cluster_name, session) if not keys_to_sync: await callback_query.message.answer( @@ -348,8 +341,6 @@ async def sync_cluster_handler(callback_query: types.CallbackQuery): .row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor")) .as_markup(), ) - finally: - await conn.close() @router.callback_query(F.data.startswith("server_availability|"), IsAdminFilter()) diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index 9a577846..31dac3bc 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -431,7 +431,7 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se await update_key_on_all_servers() - await update_key_expiry(client_id, expiry_time) + await update_key_expiry(client_id, expiry_time, session) response_message = ( f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах." diff --git a/handlers/coupons.py b/handlers/coupons.py index d010a028..6c340090 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -89,5 +89,5 @@ async def activate_coupon(user_id: int, coupon_code: str, session: Any): datetime.utcnow(), ) - await update_balance(user_id, coupon_amount) + await update_balance(user_id, coupon_amount, session) return f"✅ Купон успешно активирован! 🎉\n\nНа ваш баланс добавлено {coupon_amount} рублей 💰." diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 458ac4c9..946689de 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -158,7 +158,7 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any): expiry_time = datetime.utcnow() + timedelta(days=duration_days) await create_key(tg_id, expiry_time, None, session, callback_query) - await update_balance(tg_id, -plan_price) + await update_balance(tg_id, -plan_price, session) async def create_key( diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index ecbb1171..64a094d2 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -30,6 +30,7 @@ from database import ( delete_key, get_balance, get_key_details, + get_keys_by_server, get_servers_from_db, create_temporary_data, store_key, @@ -424,10 +425,7 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery, sessi total_gb = TOTAL_GB * gb_multiplier.get(plan, 1) if TOTAL_GB > 0 else 0 try: - record = await session.fetchrow( - "SELECT email, expiry_time FROM keys WHERE client_id = $1", - client_id, - ) + record = await get_keys_by_server(tg_id, client_id, session) if record: email = record["email"] @@ -545,8 +543,8 @@ async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_g total_gb, ) - await update_key_expiry(client_id, new_expiry_time) - await update_balance(tg_id, -cost) + await update_key_expiry(client_id, new_expiry_time, conn) + await update_balance(tg_id, -cost, conn) logger.info(f"[RENEW] Ключ {client_id} успешно продлён на {plan} мес. для пользователя {tg_id}.") await renew_key_on_cluster() diff --git a/handlers/notifications.py b/handlers/notifications.py index 67cd9890..e038e218 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -152,9 +152,9 @@ async def process_10h_record(record, bot, conn): if AUTO_RENEW_KEYS and balance >= RENEWAL_PLANS["1"]["price"]: try: - await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"]) + await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"], conn) new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000) - await update_key_expiry(record["client_id"], new_expiry_time) + await update_key_expiry(record["client_id"], new_expiry_time, conn) servers = await get_servers_from_db() for cluster_id in servers: @@ -241,9 +241,9 @@ async def process_24h_record(record, bot, conn): if AUTO_RENEW_KEYS and balance >= RENEWAL_PLANS["1"]["price"]: try: - await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"]) + await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"], conn) new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000) - await update_key_expiry(record["client_id"], new_expiry_time) + await update_key_expiry(record["client_id"], new_expiry_time, conn) servers = await get_servers_from_db() for cluster_id in servers: @@ -433,10 +433,10 @@ async def process_key(record, bot, conn): try: if AUTO_RENEW_KEYS and balance >= RENEWAL_PLANS["1"]["price"]: - await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"]) + await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"], conn) new_expiry_time = int((datetime.now(moscow_tz) + timedelta(days=30)).timestamp() * 1000) - await update_key_expiry(client_id, new_expiry_time) + await update_key_expiry(client_id, new_expiry_time, conn) servers = await get_servers_from_db() From 7dafb47c38a2a1caa21fc6ccee2cf56bb9c4a9c6 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 23 Jan 2025 11:28:21 +0300 Subject: [PATCH 30/49] Enhance database interaction in coupon and server management. Updated `check_unique_server_name` to accept session and cluster name parameters for improved uniqueness checks. Introduced new functions: `get_coupon_by_code`, `delete_server`, `create_coupon_usage`, `check_coupon_usage`, and `update_coupon_usage_count` for better coupon management. Refactored handlers to utilize these new functions, improving code clarity and maintainability. --- database.py | 160 ++++++++++++++++++++++++++++++-- handlers/admin/admin_servers.py | 17 +--- handlers/coupons.py | 42 ++------- servers.py | 11 +-- 4 files changed, 167 insertions(+), 63 deletions(-) diff --git a/database.py b/database.py index 688f0ebf..a833e664 100644 --- a/database.py +++ b/database.py @@ -75,18 +75,26 @@ async def init_db(file_path: str = "assets/schema.sql"): await conn.close() -async def check_unique_server_name(server_name: str) -> bool: +async def check_unique_server_name(server_name: str, session: Any, cluster_name: str | None = None) -> bool: """ Проверяет уникальность имени сервера. :param server_name: Имя сервера. + :param session: Сессия базы данных. + :param cluster_name: Имя кластера (опционально). :return: True, если имя сервера уникально, False, если уже существует. """ - conn = await asyncpg.connect(DATABASE_URL) - - result = await conn.fetchrow("SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", server_name) - - await conn.close() + if cluster_name: + result = await session.fetchrow( + "SELECT 1 FROM servers WHERE server_name = $1 AND cluster_name = $2 LIMIT 1", + server_name, + cluster_name + ) + else: + result = await session.fetchrow( + "SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", + server_name + ) return result is None @@ -122,6 +130,39 @@ async def create_coupon(coupon_code: str, amount: float, usage_limit: int, sessi logger.error(f"Ошибка при создании купона {coupon_code}: {e}") raise +async def get_coupon_by_code(coupon_code: str, session: Any) -> dict | None: + """ + Получает информацию о купоне по его коду. + + Args: + coupon_code (str): Код купона для поиска + session (Any): Сессия базы данных + + Returns: + dict | None: Словарь с информацией о купоне или None, если купон не найден + - id (int): ID купона + - usage_limit (int): Лимит использований + - usage_count (int): Текущее количество использований + - is_used (bool): Флаг использования + - amount (float): Сумма купона + + Raises: + Exception: В случае ошибки при выполнении запроса + """ + try: + result = await session.fetchrow( + """ + SELECT id, usage_limit, usage_count, is_used, amount + FROM coupons + WHERE code = $1 AND (usage_count < usage_limit OR usage_limit = 0) AND is_used = FALSE + """, + coupon_code, + ) + return dict(result) if result else None + except Exception as e: + logger.error(f"Ошибка при получении купона {coupon_code}: {e}") + raise + async def get_all_coupons(session: Any, page: int = 1, per_page: int = 10): """ @@ -1259,3 +1300,110 @@ async def add_server_to_db( except Exception as e: logger.error(f"Ошибка при добавлении сервера {server_name} в кластер {cluster_name}: {e}") raise + +async def delete_server(server_name: str, session: Any): + """ + Удаляет сервер из базы данных по его названию. + + Args: + server_name (str): Название сервера для удаления + session (Any): Сессия базы данных + + Raises: + Exception: В случае ошибки при удалении сервера + """ + try: + await session.execute( + """ + DELETE FROM servers WHERE server_name = $1 + """, + server_name, + ) + logger.info(f"Сервер {server_name} успешно удалён из базы данных") + except Exception as e: + logger.error(f"Ошибка при удалении сервера {server_name} из базы данных: {e}") + raise + + +async def create_coupon_usage(coupon_id: int, user_id: int, session: Any): + """ + Создаёт запись об использовании купона в базе данных. + + Args: + coupon_id (int): ID купона + user_id (int): ID пользователя + session (Any): Сессия базы данных + + Raises: + Exception: В случае ошибки при создании записи + """ + try: + await session.execute( + """ + INSERT INTO coupon_usages (coupon_id, user_id, used_at) + VALUES ($1, $2, $3) + """, + coupon_id, + user_id, + datetime.utcnow(), + ) + logger.info(f"Создана запись об использовании купона {coupon_id} пользователем {user_id}") + except Exception as e: + logger.error(f"Ошибка при создании записи об использовании купона {coupon_id} пользователем {user_id}: {e}") + raise + +async def check_coupon_usage(coupon_id: int, user_id: int, session: Any) -> bool: + """ + Проверяет, использовал ли пользователь данный купон. + + Args: + coupon_id (int): ID купона для проверки + user_id (int): ID пользователя для проверки + session (Any): Сессия базы данных + + Returns: + bool: True если купон уже использован, False если нет + + Raises: + Exception: В случае ошибки при выполнении запроса + """ + try: + result = await session.fetchrow( + """ + SELECT 1 FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2 + """, + coupon_id, + user_id, + ) + return result is not None + except Exception as e: + logger.error(f"Ошибка при проверке использования купона {coupon_id} пользователем {user_id}: {e}") + raise + + +async def update_coupon_usage_count(coupon_id: int, session: Any): + """ + Обновляет счетчик использования купона и его статус. + + Args: + coupon_id (int): ID купона для обновления + session (Any): Сессия базы данных + + Raises: + Exception: В случае ошибки при обновлении данных купона + """ + try: + await session.execute( + """ + UPDATE coupons + SET usage_count = usage_count + 1, + is_used = CASE WHEN usage_count + 1 >= usage_limit AND usage_limit > 0 THEN TRUE ELSE FALSE END + WHERE id = $1 + """, + coupon_id, + ) + logger.info(f"Успешно обновлен счетчик использования купона {coupon_id}") + except Exception as e: + logger.error(f"Ошибка при обновлении счетчика использования купона {coupon_id}: {e}") + raise + diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index c110fda7..fd429596 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -11,7 +11,7 @@ from py3xui import AsyncApi from backup import create_backup_and_send_to_admins from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL -from database import add_server_to_db, check_unique_server_name, get_keys_by_server, get_servers_from_db +from database import add_server_to_db, check_unique_server_name, delete_server, get_keys_by_server, get_servers_from_db from filters.admin import IsAdminFilter from handlers.keys.key_utils import create_key_on_cluster from logger import logger @@ -93,7 +93,7 @@ async def handle_cluster_name_input(message: types.Message, state: FSMContext): @router.message(UserEditorState.waiting_for_server_name, IsAdminFilter()) -async def handle_server_name_input(message: types.Message, state: FSMContext): +async def handle_server_name_input(message: types.Message, state: FSMContext, session: Any): server_name = message.text.strip() if server_name == "❌ Отменить": @@ -110,7 +110,7 @@ async def handle_server_name_input(message: types.Message, state: FSMContext): await message.answer("❌ Имя сервера не может быть пустым. Попробуйте снова.") return - server_unique = await check_unique_server_name(server_name) + server_unique = await check_unique_server_name(server_name, session) if not server_unique: await message.answer("❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя.") return @@ -432,17 +432,10 @@ async def handle_delete_server(callback_query: types.CallbackQuery, state: FSMCo @router.callback_query(F.data.startswith("confirm_delete_server|"), IsAdminFilter()) -async def handle_confirm_delete_server(callback_query: types.CallbackQuery, state: FSMContext): +async def handle_confirm_delete_server(callback_query: types.CallbackQuery, state: FSMContext, session: Any): server_name = callback_query.data.split("|")[1] - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( - """ - DELETE FROM servers WHERE server_name = $1 - """, - server_name, - ) - await conn.close() + await delete_server(server_name, session) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад в управление кластерами", callback_data="servers_editor")) diff --git a/handlers/coupons.py b/handlers/coupons.py index 6c340090..6c74359e 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -7,7 +7,7 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from database import update_balance +from database import check_coupon_usage, create_coupon_usage, get_coupon_by_code, update_balance, update_coupon_usage_count class CouponActivationState(StatesGroup): @@ -43,51 +43,21 @@ async def process_coupon_code(message: types.Message, state: FSMContext, session async def activate_coupon(user_id: int, coupon_code: str, session: Any): - coupon_record = await session.fetchrow( - """ - SELECT id, usage_limit, usage_count, is_used, amount - FROM coupons - WHERE code = $1 AND (usage_count < usage_limit OR usage_limit = 0) AND is_used = FALSE - """, - coupon_code, - ) + coupon_record = await get_coupon_by_code(coupon_code, session) if not coupon_record: return "❌ Купон не найден 🚫 или его использование ограничено. 🔒 Пожалуйста, проверьте код и попробуйте снова. 🔍" - usage_exists = await session.fetchrow( - """ - SELECT 1 FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2 - """, - coupon_record["id"], - user_id, - ) + usage_exists = await check_coupon_usage(coupon_record["id"], user_id, session) + if usage_exists: return "❌ Вы уже активировали этот купон. 🚫 Купоны могут быть активированы только один раз. 🔒" coupon_amount = coupon_record["amount"] - async with session.transaction(): - await session.execute( - """ - UPDATE coupons - SET usage_count = usage_count + 1, - is_used = CASE WHEN usage_count + 1 >= usage_limit AND usage_limit > 0 THEN TRUE ELSE FALSE END - WHERE id = $1 - """, - coupon_record["id"], - ) - - await session.execute( - """ - INSERT INTO coupon_usages (coupon_id, user_id, used_at) - VALUES ($1, $2, $3) - """, - coupon_record["id"], - user_id, - datetime.utcnow(), - ) + await update_coupon_usage_count(coupon_record["id"], session) + await create_coupon_usage(coupon_record["id"], user_id, session) await update_balance(user_id, coupon_amount, session) return f"✅ Купон успешно активирован! 🎉\n\nНа ваш баланс добавлено {coupon_amount} рублей 💰." diff --git a/servers.py b/servers.py index af439a0b..f97b7bbb 100644 --- a/servers.py +++ b/servers.py @@ -9,7 +9,7 @@ from ping3 import ping from bot import bot from config import ADMIN_ID, DATABASE_URL, PING_TIME -from database import add_server_to_db, get_servers_from_db +from database import add_server_to_db, check_unique_server_name, get_servers_from_db from logger import logger try: @@ -34,14 +34,7 @@ async def sync_servers_with_db(): for cluster_name, servers in CLUSTERS.items(): for server_key, server_info in servers.items(): - exists = await conn.fetchval( - """ - SELECT 1 FROM servers - WHERE cluster_name = $1 AND server_name = $2 - """, - cluster_name, - server_info["name"], - ) + exists = await check_unique_server_name(server_info["name"], conn, cluster_name) if not exists: await add_server_to_db( From 4cfaf419801f52e9ed1267e4fda512cb166be869 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 23 Jan 2025 08:28:39 +0000 Subject: [PATCH 31/49] Auto-format code with Ruff using pyproject.toml --- database.py | 13 +++++-------- handlers/coupons.py | 9 +++++++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/database.py b/database.py index a833e664..4769bf78 100644 --- a/database.py +++ b/database.py @@ -86,15 +86,10 @@ async def check_unique_server_name(server_name: str, session: Any, cluster_name: """ if cluster_name: result = await session.fetchrow( - "SELECT 1 FROM servers WHERE server_name = $1 AND cluster_name = $2 LIMIT 1", - server_name, - cluster_name + "SELECT 1 FROM servers WHERE server_name = $1 AND cluster_name = $2 LIMIT 1", server_name, cluster_name ) else: - result = await session.fetchrow( - "SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", - server_name - ) + result = await session.fetchrow("SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", server_name) return result is None @@ -130,6 +125,7 @@ async def create_coupon(coupon_code: str, amount: float, usage_limit: int, sessi logger.error(f"Ошибка при создании купона {coupon_code}: {e}") raise + async def get_coupon_by_code(coupon_code: str, session: Any) -> dict | None: """ Получает информацию о купоне по его коду. @@ -1301,6 +1297,7 @@ async def add_server_to_db( logger.error(f"Ошибка при добавлении сервера {server_name} в кластер {cluster_name}: {e}") raise + async def delete_server(server_name: str, session: Any): """ Удаляет сервер из базы данных по его названию. @@ -1352,6 +1349,7 @@ async def create_coupon_usage(coupon_id: int, user_id: int, session: Any): logger.error(f"Ошибка при создании записи об использовании купона {coupon_id} пользователем {user_id}: {e}") raise + async def check_coupon_usage(coupon_id: int, user_id: int, session: Any) -> bool: """ Проверяет, использовал ли пользователь данный купон. @@ -1406,4 +1404,3 @@ async def update_coupon_usage_count(coupon_id: int, session: Any): except Exception as e: logger.error(f"Ошибка при обновлении счетчика использования купона {coupon_id}: {e}") raise - diff --git a/handlers/coupons.py b/handlers/coupons.py index 6c74359e..b652cf1c 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -7,7 +7,13 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from database import check_coupon_usage, create_coupon_usage, get_coupon_by_code, update_balance, update_coupon_usage_count +from database import ( + check_coupon_usage, + create_coupon_usage, + get_coupon_by_code, + update_balance, + update_coupon_usage_count, +) class CouponActivationState(StatesGroup): @@ -50,7 +56,6 @@ async def activate_coupon(user_id: int, coupon_code: str, session: Any): usage_exists = await check_coupon_usage(coupon_record["id"], user_id, session) - if usage_exists: return "❌ Вы уже активировали этот купон. 🚫 Купоны могут быть активированы только один раз. 🔒" From 73a365eeb6240b6442784cc463411e1f53293d83 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 23 Jan 2025 11:36:15 +0300 Subject: [PATCH 32/49] Refactor server management and database interactions. Renamed `get_servers_from_db` to `get_servers` for clarity and updated related handlers to utilize session management. Refactored `add_server_to_db` to `create_server` for consistency. Improved code readability and maintainability across coupon and notification handlers by streamlining database calls and enhancing error handling. --- database.py | 68 ++++++++++++++--------------- handlers/admin/admin_servers.py | 26 +++++------ handlers/admin/admin_user_editor.py | 8 ++-- handlers/coupons.py | 9 +++- handlers/keys/key_utils.py | 10 ++--- handlers/keys/keys.py | 4 +- handlers/keys/subscriptions.py | 6 +-- handlers/keys/trial_key.py | 4 +- handlers/notifications.py | 12 ++--- handlers/utils.py | 4 +- servers.py | 6 +-- 11 files changed, 81 insertions(+), 76 deletions(-) diff --git a/database.py b/database.py index a833e664..fee9d8da 100644 --- a/database.py +++ b/database.py @@ -86,15 +86,10 @@ async def check_unique_server_name(server_name: str, session: Any, cluster_name: """ if cluster_name: result = await session.fetchrow( - "SELECT 1 FROM servers WHERE server_name = $1 AND cluster_name = $2 LIMIT 1", - server_name, - cluster_name + "SELECT 1 FROM servers WHERE server_name = $1 AND cluster_name = $2 LIMIT 1", server_name, cluster_name ) else: - result = await session.fetchrow( - "SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", - server_name - ) + result = await session.fetchrow("SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", server_name) return result is None @@ -130,6 +125,7 @@ async def create_coupon(coupon_code: str, amount: float, usage_limit: int, sessi logger.error(f"Ошибка при создании купона {coupon_code}: {e}") raise + async def get_coupon_by_code(coupon_code: str, session: Any) -> dict | None: """ Получает информацию о купоне по его коду. @@ -1101,34 +1097,37 @@ async def check_notification_time(tg_id: int, notification_type: str, hours: int await conn.close() -async def get_servers_from_db(): - conn = await asyncpg.connect(DATABASE_URL) +async def get_servers(session: Any = None): + conn = None + try: + conn = session if session is not None else await asyncpg.connect(DATABASE_URL) - result = await conn.fetch( - """ - SELECT cluster_name, server_name, api_url, subscription_url, inbound_id - FROM servers - """ - ) - - await conn.close() - - servers = {} - for row in result: - cluster_name = row["cluster_name"] - if cluster_name not in servers: - servers[cluster_name] = [] - - servers[cluster_name].append( - { - "server_name": row["server_name"], - "api_url": row["api_url"], - "subscription_url": row["subscription_url"], - "inbound_id": row["inbound_id"], - } + result = await conn.fetch( + """ + SELECT cluster_name, server_name, api_url, subscription_url, inbound_id + FROM servers + """ ) + servers = {} + for row in result: + cluster_name = row["cluster_name"] + if cluster_name not in servers: + servers[cluster_name] = [] - return servers + servers[cluster_name].append( + { + "server_name": row["server_name"], + "api_url": row["api_url"], + "subscription_url": row["subscription_url"], + "inbound_id": row["inbound_id"], + } + ) + + return servers + + finally: + if conn is not None and session is None: + await conn.close() async def delete_user_data(session: Any, tg_id: int): @@ -1267,7 +1266,7 @@ async def delete_key(identifier, session): logger.error(f"Ошибка при удалении ключа с идентификатором {identifier} из базы данных: {e}") -async def add_server_to_db( +async def create_server( cluster_name: str, server_name: str, api_url: str, subscription_url: str, inbound_id: int, session: Any ): """ @@ -1301,6 +1300,7 @@ async def add_server_to_db( logger.error(f"Ошибка при добавлении сервера {server_name} в кластер {cluster_name}: {e}") raise + async def delete_server(server_name: str, session: Any): """ Удаляет сервер из базы данных по его названию. @@ -1352,6 +1352,7 @@ async def create_coupon_usage(coupon_id: int, user_id: int, session: Any): logger.error(f"Ошибка при создании записи об использовании купона {coupon_id} пользователем {user_id}: {e}") raise + async def check_coupon_usage(coupon_id: int, user_id: int, session: Any) -> bool: """ Проверяет, использовал ли пользователь данный купон. @@ -1406,4 +1407,3 @@ async def update_coupon_usage_count(coupon_id: int, session: Any): except Exception as e: logger.error(f"Ошибка при обновлении счетчика использования купона {coupon_id}: {e}") raise - diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index fd429596..e0d27e0d 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -11,7 +11,7 @@ from py3xui import AsyncApi from backup import create_backup_and_send_to_admins from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL -from database import add_server_to_db, check_unique_server_name, delete_server, get_keys_by_server, get_servers_from_db +from database import create_server, check_unique_server_name, delete_server, get_keys_by_server, get_servers from filters.admin import IsAdminFilter from handlers.keys.key_utils import create_key_on_cluster from logger import logger @@ -28,8 +28,8 @@ class UserEditorState(StatesGroup): @router.callback_query(F.data == "servers_editor", IsAdminFilter()) -async def handle_servers_editor(callback_query: types.CallbackQuery): - servers = await get_servers_from_db() +async def handle_servers_editor(callback_query: types.CallbackQuery, session: Any): + servers = await get_servers(session) builder = InlineKeyboardBuilder() @@ -225,7 +225,7 @@ async def handle_inbound_id_input(message: types.Message, state: FSMContext, ses api_url = user_data.get("api_url") subscription_url = user_data.get("subscription_url") - await add_server_to_db( + await create_server( cluster_name=cluster_name, server_name=server_name, api_url=api_url, @@ -246,10 +246,10 @@ async def handle_inbound_id_input(message: types.Message, state: FSMContext, ses @router.callback_query(F.data.startswith("manage_cluster|"), IsAdminFilter()) -async def handle_manage_cluster(callback_query: types.CallbackQuery, state: FSMContext): +async def handle_manage_cluster(callback_query: types.CallbackQuery, state: FSMContext, session: Any): cluster_name = callback_query.data.split("|")[1] - servers = await get_servers_from_db() + servers = await get_servers(session) cluster_servers = servers.get(cluster_name, []) builder = InlineKeyboardBuilder() @@ -310,7 +310,7 @@ async def sync_cluster_handler(callback_query: types.CallbackQuery, session: Any ) return - servers = await get_servers_from_db() + servers = await get_servers(session) cluster_servers = servers.get(cluster_name, []) for key in keys_to_sync: @@ -344,10 +344,10 @@ async def sync_cluster_handler(callback_query: types.CallbackQuery, session: Any @router.callback_query(F.data.startswith("server_availability|"), IsAdminFilter()) -async def handle_check_server_availability(callback_query: types.CallbackQuery): +async def handle_check_server_availability(callback_query: types.CallbackQuery, session: Any): cluster_name = callback_query.data.split("|")[1] - servers = await get_servers_from_db() + servers = await get_servers(session) cluster_servers = servers.get(cluster_name, []) if not cluster_servers: @@ -382,10 +382,10 @@ async def handle_check_server_availability(callback_query: types.CallbackQuery): @router.callback_query(F.data.startswith("manage_server|"), IsAdminFilter()) -async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMContext): +async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMContext, session: Any): server_name = callback_query.data.split("|")[1] - servers = await get_servers_from_db() + servers = await get_servers(session) server = None cluster_name = None @@ -463,10 +463,10 @@ async def handle_add_server(callback_query: types.CallbackQuery, state: FSMConte @router.callback_query(F.data.startswith("backup_cluster|"), IsAdminFilter()) -async def handle_backup_cluster(callback_query: types.CallbackQuery): +async def handle_backup_cluster(callback_query: types.CallbackQuery, session: Any): cluster_name = callback_query.data.split("|")[1] - servers = await get_servers_from_db() + servers = await get_servers(session) cluster_servers = servers.get(cluster_name, []) for server in cluster_servers: diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index 31dac3bc..87c5193d 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -15,7 +15,7 @@ from database import ( get_client_id_by_email, get_key_details, get_keys, - get_servers_from_db, + get_servers, set_trial, update_key_expiry, ) @@ -410,7 +410,7 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se await state.clear() return - clusters = await get_servers_from_db() + clusters = await get_servers(session) async def update_key_on_all_servers(): tasks = [] @@ -488,7 +488,7 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery, s builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys")) - clusters = await get_servers_from_db() + clusters = await get_servers(session) async def delete_key_from_servers(email, client_id): tasks = [] @@ -569,7 +569,7 @@ async def delete_user(callback_query: types.CallbackQuery, session: Any): try: tasks = [] for email, client_id in key_records: - servers = await get_servers_from_db() + servers = await get_servers(session) for cluster_id, cluster in servers.items(): tasks.append(delete_key_from_cluster(cluster_id, email, client_id)) await asyncio.gather(*tasks) diff --git a/handlers/coupons.py b/handlers/coupons.py index 6c74359e..b652cf1c 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -7,7 +7,13 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from database import check_coupon_usage, create_coupon_usage, get_coupon_by_code, update_balance, update_coupon_usage_count +from database import ( + check_coupon_usage, + create_coupon_usage, + get_coupon_by_code, + update_balance, + update_coupon_usage_count, +) class CouponActivationState(StatesGroup): @@ -50,7 +56,6 @@ async def activate_coupon(user_id: int, coupon_code: str, session: Any): usage_exists = await check_coupon_usage(coupon_record["id"], user_id, session) - if usage_exists: return "❌ Вы уже активировали этот купон. 🚫 Купоны могут быть активированы только один раз. 🔒" diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index 98877801..00e7098c 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -4,7 +4,7 @@ from py3xui import AsyncApi from client import ClientConfig, add_client, delete_client, extend_client_key from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, SUPERNODE, TOTAL_GB -from database import get_servers_from_db +from database import get_servers from logger import logger @@ -13,7 +13,7 @@ async def create_key_on_cluster(cluster_id: str, tg_id: int, client_id: str, ema Создает ключ на всех серверах указанного кластера. """ try: - servers = await get_servers_from_db() + servers = await get_servers() cluster = servers.get(cluster_id) if not cluster: @@ -105,7 +105,7 @@ async def create_client_on_server( async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, total_gb): try: - servers = await get_servers_from_db() + servers = await get_servers() cluster = servers.get(cluster_id) if not cluster: @@ -147,7 +147,7 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to async def delete_key_from_cluster(cluster_id, email, client_id): """Удаление ключа с серверов в кластере""" try: - servers = await get_servers_from_db() + servers = await get_servers() cluster = servers.get(cluster_id) if not cluster: @@ -186,7 +186,7 @@ async def delete_key_from_cluster(cluster_id, email, client_id): async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id): try: - servers = await get_servers_from_db() + servers = await get_servers() cluster = servers.get(cluster_id) if not cluster: diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 64a094d2..8213f7eb 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -31,7 +31,7 @@ from database import ( get_balance, get_key_details, get_keys_by_server, - get_servers_from_db, + get_servers, create_temporary_data, store_key, update_balance, @@ -385,7 +385,7 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery, s reply_markup=keyboard, ) - servers = await get_servers_from_db() + servers = await get_servers(session) async def delete_key_from_servers(): try: diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index d3f6d36e..6203252b 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -7,7 +7,7 @@ import asyncpg from aiohttp import web from config import DATABASE_URL, PROJECT_NAME, SUB_MESSAGE, SUPERNODE, TRANSITION_DATE_STR -from database import get_key_details, get_servers_from_db +from database import get_key_details, get_servers from logger import logger # Глобальная переменная для пула соединений @@ -122,7 +122,7 @@ async def handle_old_subscription(request): status=400, ) - servers = await get_servers_from_db() + servers = await get_servers() cluster_servers = servers.get(cluster_name, []) logger.info(f"Сервера в кластере: {cluster_servers}") @@ -180,7 +180,7 @@ async def handle_new_subscription(request): status=403, ) - servers = await get_servers_from_db() + servers = await get_servers() cluster_servers = servers.get(cluster_name, []) urls = [f"{server['subscription_url']}/{email}" for server in cluster_servers] diff --git a/handlers/keys/trial_key.py b/handlers/keys/trial_key.py index 12725c02..1c433e5c 100644 --- a/handlers/keys/trial_key.py +++ b/handlers/keys/trial_key.py @@ -8,7 +8,7 @@ from py3xui import AsyncApi from client import ClientConfig, add_client from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, TRIAL_TIME -from database import get_servers_from_db, get_trial, store_key, set_trial +from database import get_servers, get_trial, store_key, set_trial from handlers.texts import INSTRUCTIONS from handlers.utils import generate_random_email, get_least_loaded_cluster from logger import logger @@ -33,7 +33,7 @@ async def create_trial_key(tg_id: int, session: Any): expiry_time = current_time + timedelta(days=TRIAL_TIME) expiry_timestamp = int(expiry_time.timestamp() * 1000) - clusters = await get_servers_from_db() + clusters = await get_servers(session) least_loaded_cluster = await get_least_loaded_cluster() if least_loaded_cluster not in clusters: raise ValueError(f"Кластер {least_loaded_cluster} не найден в базе данных.") diff --git a/handlers/notifications.py b/handlers/notifications.py index e038e218..35c7c2c6 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -28,7 +28,7 @@ from database import ( check_notification_time, delete_key, get_balance, - get_servers_from_db, + get_servers, update_balance, update_key_expiry, ) @@ -156,7 +156,7 @@ async def process_10h_record(record, bot, conn): new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000) await update_key_expiry(record["client_id"], new_expiry_time, conn) - servers = await get_servers_from_db() + servers = await get_servers(conn) for cluster_id in servers: await renew_key_in_cluster(cluster_id, email, record["client_id"], new_expiry_time, TOTAL_GB) logger.info(f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}.") @@ -245,7 +245,7 @@ async def process_24h_record(record, bot, conn): new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000) await update_key_expiry(record["client_id"], new_expiry_time, conn) - servers = await get_servers_from_db() + servers = await get_servers(conn) for cluster_id in servers: await renew_key_in_cluster(cluster_id, email, record["client_id"], new_expiry_time, TOTAL_GB) logger.info(f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}.") @@ -438,7 +438,7 @@ async def process_key(record, bot, conn): new_expiry_time = int((datetime.now(moscow_tz) + timedelta(days=30)).timestamp() * 1000) await update_key_expiry(client_id, new_expiry_time, conn) - servers = await get_servers_from_db() + servers = await get_servers(conn) for cluster_id in servers: await renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, TOTAL_GB) @@ -487,7 +487,7 @@ async def process_key(record, bot, conn): logger.error(f"Не удалось отправить уведомление об истечении клиенту {tg_id}: {e}") if AUTO_DELETE_EXPIRED_KEYS: - servers = await get_servers_from_db() + servers = await get_servers(conn) for cluster_id in servers: try: @@ -509,7 +509,7 @@ async def process_key(record, bot, conn): async def check_online_users(): - servers = await get_servers_from_db() + servers = await get_servers() for cluster_id, cluster in servers.items(): for server_id, server in enumerate(cluster): diff --git a/handlers/utils.py b/handlers/utils.py index 608a0f4c..dba6fb7b 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -7,7 +7,7 @@ import asyncpg from bot import bot from config import DATABASE_URL -from database import get_servers_from_db +from database import get_servers from logger import logger @@ -59,7 +59,7 @@ async def get_least_loaded_cluster() -> str: Returns: str: Идентификатор наименее загруженного кластера. """ - servers = await get_servers_from_db() + servers = await get_servers() cluster_loads: dict[str, int] = {cluster_id: 0 for cluster_id in servers.keys()} diff --git a/servers.py b/servers.py index f97b7bbb..521b55f1 100644 --- a/servers.py +++ b/servers.py @@ -9,7 +9,7 @@ from ping3 import ping from bot import bot from config import ADMIN_ID, DATABASE_URL, PING_TIME -from database import add_server_to_db, check_unique_server_name, get_servers_from_db +from database import create_server, check_unique_server_name, get_servers from logger import logger try: @@ -37,7 +37,7 @@ async def sync_servers_with_db(): exists = await check_unique_server_name(server_info["name"], conn, cluster_name) if not exists: - await add_server_to_db( + await create_server( cluster_name=cluster_name, server_name=server_info["name"], api_url=server_info["API_URL"], @@ -117,7 +117,7 @@ async def check_servers(): Периодическая проверка серверов с учетом извлечения хоста из `api_url`. """ while True: - servers = await get_servers_from_db() + servers = await get_servers() current_time = datetime.now() logger.info(f"Начинаю проверку серверов: {current_time}") From 589851ae2cccf2fdc4faed3ee21b654840d67f6e Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 23 Jan 2025 08:40:44 +0000 Subject: [PATCH 33/49] Auto-format code with Ruff using pyproject.toml --- database.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/database.py b/database.py index e1304589..11530a4f 100644 --- a/database.py +++ b/database.py @@ -1409,8 +1409,6 @@ async def update_coupon_usage_count(coupon_id: int, session: Any): raise - - async def get_last_payments(tg_id: int, session: Any): """ Получает последние 3 платежа пользователя. @@ -1434,13 +1432,10 @@ async def get_last_payments(tg_id: int, session: Any): ORDER BY created_at DESC LIMIT 3 """, - tg_id + tg_id, ) logger.info(f"Успешно получены последние платежи для пользователя {tg_id}") return records except Exception as e: logger.error(f"Ошибка при получении последних платежей для пользователя {tg_id}: {e}") raise - - - From 5be5cd33f71ffe06323b05cc9c33540bff951862 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 23 Jan 2025 11:58:35 +0300 Subject: [PATCH 34/49] Implement new database functions for server and coupon management. Added `check_server_name_by_cluster`, `get_coupon_details`, `get_referral_by_referred_id`, and `get_all_keys` to enhance data retrieval capabilities. Updated handlers to utilize these new functions, improving code clarity and maintainability. Refactored existing database calls in `start.py`, `utils.py`, and `keys.py` for consistency and better session management. --- database.py | 130 +++++++++++++++++++++++++++++++++++++++++- handlers/keys/keys.py | 20 ++----- handlers/start.py | 12 ++-- handlers/utils.py | 4 +- 4 files changed, 137 insertions(+), 29 deletions(-) diff --git a/database.py b/database.py index e1304589..ed8cb718 100644 --- a/database.py +++ b/database.py @@ -94,6 +94,37 @@ async def check_unique_server_name(server_name: str, session: Any, cluster_name: return result is None +async def check_server_name_by_cluster(server_name: str, session: Any) -> dict | None: + """ + Проверяет принадлежность сервера к кластеру. + + Args: + server_name (str): Имя сервера для проверки + session (Any): Сессия базы данных + + Returns: + dict | None: Словарь с информацией о кластере или None, если сервер не найден + - cluster_name (str): Название кластера + """ + try: + cluster_info = await session.fetchrow( + """ + SELECT cluster_name + FROM servers + WHERE server_name = $1 + """, + server_name, + ) + if cluster_info: + logger.info(f"Найден кластер для сервера {server_name}") + return dict(cluster_info) + logger.info(f"Кластер для сервера {server_name} не найден") + return None + except Exception as e: + logger.error(f"Ошибка при поиске кластера для сервера {server_name}: {e}") + raise + + async def create_coupon(coupon_code: str, amount: float, usage_limit: int, session: Any): """ Создает новый купон в базе данных. @@ -1409,8 +1440,6 @@ async def update_coupon_usage_count(coupon_id: int, session: Any): raise - - async def get_last_payments(tg_id: int, session: Any): """ Получает последние 3 платежа пользователя. @@ -1434,7 +1463,7 @@ async def get_last_payments(tg_id: int, session: Any): ORDER BY created_at DESC LIMIT 3 """, - tg_id + tg_id, ) logger.info(f"Успешно получены последние платежи для пользователя {tg_id}") return records @@ -1443,4 +1472,99 @@ async def get_last_payments(tg_id: int, session: Any): raise +async def get_coupon_details(coupon_id: str, session: Any): + """ + Получает детали купона по его ID. + Args: + coupon_id (str): ID купона + session (Any): Сессия базы данных + + Returns: + dict: Словарь с деталями купона или None если купон не найден + + Raises: + Exception: В случае ошибки при выполнении запроса + """ + try: + record = await session.fetchrow( + """ + SELECT id, code, discount, usage_count, usage_limit, is_used + FROM coupons + WHERE id = $1 + """, + coupon_id, + ) + + if record: + logger.info(f"Успешно получены детали купона {coupon_id}") + return dict(record) + + logger.warning(f"Купон {coupon_id} не найден") + return None + + except Exception as e: + logger.error(f"Ошибка при получении деталей купона {coupon_id}: {e}") + raise + + +async def get_referral_by_referred_id(referred_tg_id: int, session: Any): + """ + Получает информацию о реферале по ID приглашенного пользователя. + + Args: + referred_tg_id (int): ID приглашенного пользователя + session (Any): Сессия базы данных + + Returns: + dict: Словарь с информацией о реферале или None если не найден + + Raises: + Exception: В случае ошибки при выполнении запроса + """ + try: + record = await session.fetchrow( + """ + SELECT * FROM referrals + WHERE referred_tg_id = $1 + """, + referred_tg_id, + ) + + if record: + logger.info(f"Успешно получена информация о реферале для пользователя {referred_tg_id}") + return dict(record) + + logger.info(f"Реферал для пользователя {referred_tg_id} не найден") + return None + + except Exception as e: + logger.error(f"Ошибка при получении информации о реферале для пользователя {referred_tg_id}: {e}") + raise + + +async def get_all_keys(session: Any = None): + """ + Получает все записи из таблицы keys. + + Args: + session (Any, optional): Сессия базы данных. По умолчанию None. + + Returns: + list: Список всех записей из таблицы keys + + Raises: + Exception: В случае ошибки при выполнении запроса + """ + conn = None + try: + conn = session if session is not None else await asyncpg.connect(DATABASE_URL) + keys = await conn.fetch("SELECT * FROM keys") + logger.info(f"Успешно получены все записи из таблицы keys. Количество: {len(keys)}") + return keys + except Exception as e: + logger.error(f"Ошибка при получении записей из таблицы keys: {e}") + raise + finally: + if conn is not None and session is None: + await conn.close() diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 8213f7eb..bece4e4f 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -27,9 +27,11 @@ from config import ( USE_NEW_PAYMENT_FLOW, ) from database import ( + check_server_name_by_cluster, delete_key, get_balance, get_key_details, + get_keys, get_keys_by_server, get_servers, create_temporary_data, @@ -83,14 +85,7 @@ async def process_callback_or_message_view_keys( send_photo = callback_query_or_message.answer_photo try: - records = await session.fetch( - """ - SELECT email, client_id, expiry_time - FROM keys - WHERE tg_id = $1 - """, - chat_id, - ) + records = await get_keys(chat_id, session) inline_keyboard, response_message = build_keys_response(records) @@ -512,14 +507,7 @@ async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_g server_id = key_info["server_id"] if USE_COUNTRY_SELECTION: - cluster_info = await conn.fetchrow( - """ - SELECT cluster_name - FROM servers - WHERE server_name = $1 - """, - server_id, - ) + cluster_info = await check_server_name_by_cluster(server_id, conn) if not cluster_info: logger.error(f"[RENEW] Сервер {server_id} не найден в таблице servers.") diff --git a/handlers/start.py b/handlers/start.py index 25b83bfd..de10c227 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -28,6 +28,8 @@ from database import ( add_connection, add_referral, check_connection_exists, + get_coupon_details, + get_referral_by_referred_id, get_trial, set_trial, ) @@ -84,10 +86,7 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin recipient_tg_id = message.chat.id - gift_info = await session.fetchrow( - "SELECT * FROM gifts WHERE gift_id = $1 AND is_used = FALSE", - gift_id, - ) + gift_info = await get_coupon_details(gift_id, session) if gift_info is None: logger.warning(f"Подарок с ID {gift_id} уже был использован или не существует.") @@ -137,10 +136,7 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin await message.answer("❌ Вы не можете быть рефералом самого себя.") return await show_start_menu(message, admin, session) - existing_referral = await session.fetchrow( - "SELECT * FROM referrals WHERE referred_tg_id = $1", - message.chat.id, - ) + existing_referral = await get_referral_by_referred_id(message.chat.id, session) if existing_referral: logger.info(f"Реферал с ID {message.chat.id} уже существует.") diff --git a/handlers/utils.py b/handlers/utils.py index dba6fb7b..29d525f7 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -7,7 +7,7 @@ import asyncpg from bot import bot from config import DATABASE_URL -from database import get_servers +from database import get_all_keys, get_servers from logger import logger @@ -65,7 +65,7 @@ async def get_least_loaded_cluster() -> str: async with asyncpg.create_pool(DATABASE_URL) as pool: async with pool.acquire() as conn: - keys = await conn.fetch("SELECT server_id FROM keys") + keys = await get_all_keys(conn) for key in keys: cluster_id = key["server_id"] if cluster_id in cluster_loads: From 39b998e6df6c064958ca35d8d0ec23cd583a68e1 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 23 Jan 2025 09:00:26 +0000 Subject: [PATCH 35/49] Auto-format code with Ruff using pyproject.toml --- database.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/database.py b/database.py index d869dfb0..81e765fd 100644 --- a/database.py +++ b/database.py @@ -1568,6 +1568,8 @@ async def get_all_keys(session: Any = None): finally: if conn is not None and session is None: await conn.close() + + import json from datetime import datetime from typing import Any From 0890d326a43b115d88cc75f8c6c2d45683331fc7 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 23 Jan 2025 12:03:01 +0300 Subject: [PATCH 36/49] Refactor trial management function names for clarity. Renamed `set_trial` to `update_trial` in `database.py` and updated all related handler calls in `start.py`, `admin_user_editor.py`, and `trial_key.py` to reflect this change. This improves code readability and consistency across the codebase. --- database.py | 4 +++- handlers/admin/admin_user_editor.py | 4 ++-- handlers/keys/trial_key.py | 4 ++-- handlers/start.py | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/database.py b/database.py index d869dfb0..b526d510 100644 --- a/database.py +++ b/database.py @@ -279,7 +279,7 @@ async def delete_coupon(coupon_code: str, session: Any): return False -async def set_trial(tg_id: int, status: int, session: Any): +async def update_trial(tg_id: int, status: int, session: Any): """ Устанавливает статус триального периода для пользователя. @@ -1568,6 +1568,8 @@ async def get_all_keys(session: Any = None): finally: if conn is not None and session is None: await conn.close() + + import json from datetime import datetime from typing import Any diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index 87c5193d..6deb2aa1 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -16,7 +16,7 @@ from database import ( get_key_details, get_keys, get_servers, - set_trial, + update_trial, update_key_expiry, ) from filters.admin import IsAdminFilter @@ -207,7 +207,7 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext, session: async def handle_restore_trial(callback_query: types.CallbackQuery, session: Any): tg_id = int(callback_query.data.split("_")[2]) - await set_trial(tg_id, 0, session) + await update_trial(tg_id, 0, session) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад в меню администратора", callback_data="admin")) diff --git a/handlers/keys/trial_key.py b/handlers/keys/trial_key.py index 1c433e5c..44745102 100644 --- a/handlers/keys/trial_key.py +++ b/handlers/keys/trial_key.py @@ -8,7 +8,7 @@ from py3xui import AsyncApi from client import ClientConfig, add_client from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, TRIAL_TIME -from database import get_servers, get_trial, store_key, set_trial +from database import get_servers, get_trial, store_key, update_trial from handlers.texts import INSTRUCTIONS from handlers.utils import generate_random_email, get_least_loaded_cluster from logger import logger @@ -83,5 +83,5 @@ async def create_trial_key(tg_id: int, session: Any): session=session, ) - await set_trial(tg_id, 1, session) + await update_trial(tg_id, 1, session) return result diff --git a/handlers/start.py b/handlers/start.py index de10c227..2eac8a4b 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -31,7 +31,7 @@ from database import ( get_coupon_details, get_referral_by_referred_id, get_trial, - set_trial, + update_trial, ) from handlers.buttons.add_subscribe import ( DOWNLOAD_ANDROID_BUTTON, @@ -212,7 +212,7 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session: Any): if "error" in trial_key_info: await callback_query.message.answer(trial_key_info["error"]) else: - await set_trial(user_id, 1, session) + await update_trial(user_id, 1, session) key_message = ( f"🔑 Ваш персональный ключ доступа:\n" From 46ec8fef5068ddadd6171101ae0c1ff896b7dd0a Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 23 Jan 2025 12:06:38 +0300 Subject: [PATCH 37/49] =?UTF-8?q?Update=20button=20labels=20from=20"?= =?UTF-8?q?=F0=9F=94=99=20=D0=9D=D0=B0=D0=B7=D0=B0=D0=B4"=20to=20"?= =?UTF-8?q?=E2=AC=85=EF=B8=8F=20=D0=9D=D0=B0=D0=B7=D0=B0=D0=B4"=20across?= =?UTF-8?q?=20multiple=20admin=20handlers=20and=20key=20management=20files?= =?UTF-8?q?=20for=20improved=20user=20interface=20consistency.=20This=20ch?= =?UTF-8?q?ange=20enhances=20the=20visual=20clarity=20of=20navigation=20op?= =?UTF-8?q?tions=20within=20the=20application.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- handlers/admin/admin_coupons.py | 12 ++++---- handlers/admin/admin_panel.py | 8 ++--- handlers/admin/admin_servers.py | 20 ++++++------ handlers/admin/admin_user_editor.py | 48 ++++++++++++++--------------- handlers/buttons/gifts.py | 2 +- handlers/keys/key_management.py | 4 +-- handlers/keys/keys.py | 4 +-- handlers/payments/robokassa_pay.py | 2 +- 8 files changed, 50 insertions(+), 50 deletions(-) diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index 78b9ba9f..8a991ec2 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -24,7 +24,7 @@ async def show_coupon_management_menu(callback_query: types.CallbackQuery, state builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="➕ Создать купон", callback_data="create_coupon")) builder.row(InlineKeyboardButton(text="🎟️ Купоны", callback_data="coupons")) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="admin")) await callback_query.message.answer("🛠 Меню управления купонами:", reply_markup=builder.as_markup()) @@ -40,7 +40,7 @@ async def show_coupon_list(callback_query: types.CallbackQuery, session: Any): if not coupons: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="coupons_editor")) await callback_query.message.answer( "❌ На данный момент нет доступных купонов. 🚫\nВы можете вернуться в меню управления. 🔙", reply_markup=builder.as_markup(), @@ -68,7 +68,7 @@ async def show_coupon_list(callback_query: types.CallbackQuery, session: Any): if current_page < total_pages: builder.row(InlineKeyboardButton(text="➡️ Следующая", callback_data=f"coupons:{current_page + 1}")) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="coupons_editor")) await callback_query.message.answer(coupon_list, reply_markup=builder.as_markup()) except Exception as e: @@ -98,7 +98,7 @@ async def handle_delete_coupon(callback_query: types.CallbackQuery, session: Any @router.callback_query(F.data == "create_coupon", IsAdminFilter()) async def handle_create_coupon(callback_query: types.CallbackQuery, state: FSMContext): builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="coupons_editor")) await callback_query.message.answer( "🎫 Введите данные для создания купона в формате:\n\n" @@ -116,7 +116,7 @@ async def process_coupon_data(message: types.Message, state: FSMContext, session parts = text.split() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="coupons_editor")) if len(parts) != 3: await message.answer( @@ -149,7 +149,7 @@ async def process_coupon_data(message: types.Message, state: FSMContext, session ) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="coupons_editor")) await message.answer(result_message, reply_markup=builder.as_markup()) await state.clear() diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index 29af84ea..e3cfb0ba 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -138,7 +138,7 @@ async def user_stats_menu(callback_query: CallbackQuery, session: Any): @router.callback_query(F.data == "export_users_csv", IsAdminFilter()) async def export_users_csv(callback_query: CallbackQuery, session: Any): builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_stats")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_stats")) try: users = await session.fetch( """ @@ -187,7 +187,7 @@ async def export_users_csv(callback_query: CallbackQuery, session: Any): @router.callback_query(F.data == "export_payments_csv", IsAdminFilter()) async def export_payments_csv(callback_query: CallbackQuery, session: Any): builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_stats")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_stats")) try: payments = await session.fetch( """ @@ -238,7 +238,7 @@ async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext): builder.row(InlineKeyboardButton(text="📢 Отправить с подпиской", callback_data="send_to_subscribed")) builder.row(InlineKeyboardButton(text="📢 Отправить без подписки", callback_data="send_to_unsubscribed")) builder.row(InlineKeyboardButton(text="📢 Рассылка по кластеру", callback_data="send_to_cluster")) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="admin")) await callback_query.message.answer( "✍️ Выберите группу пользователей и введите текст сообщения для рассылки:", reply_markup=builder.as_markup(), @@ -279,7 +279,7 @@ async def handle_send_to_cluster(callback_query: CallbackQuery, state: FSMContex ) ) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="send_to")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="send_to")) await callback_query.message.answer( "✍️ Выберите кластер для рассылки сообщений:", reply_markup=builder.as_markup(), diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index e0d27e0d..5c9804c9 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -37,7 +37,7 @@ async def handle_servers_editor(callback_query: types.CallbackQuery, session: An builder.row(InlineKeyboardButton(text=f"⚙️ {cluster_name}", callback_data=f"manage_cluster|{cluster_name}")) builder.row(InlineKeyboardButton(text="➕ Добавить кластер", callback_data="add_cluster")) - builder.row(InlineKeyboardButton(text="🔙 Назад в админку", callback_data="admin")) + builder.row(InlineKeyboardButton(text="⬅️ Назад в админку", callback_data="admin")) await callback_query.message.answer( "🔧 Управление кластерами\n\n" @@ -235,7 +235,7 @@ async def handle_inbound_id_input(message: types.Message, state: FSMContext, ses ) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад к кластерам", callback_data="servers_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад к кластерам", callback_data="servers_editor")) await message.answer( f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!", @@ -285,7 +285,7 @@ async def handle_manage_cluster(callback_query: types.CallbackQuery, state: FSMC ) ) - builder.row(InlineKeyboardButton(text="🔙 Назад в управление кластерами", callback_data="servers_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад в управление кластерами", callback_data="servers_editor")) await callback_query.message.answer( f"🔧 Управление серверами для кластера {cluster_name}", @@ -305,7 +305,7 @@ async def sync_cluster_handler(callback_query: types.CallbackQuery, session: Any await callback_query.message.answer( f"❌ Нет ключей для синхронизации в кластере {cluster_name}.", reply_markup=InlineKeyboardBuilder() - .row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor")) + .row(InlineKeyboardButton(text="⬅️ Назад", callback_data="servers_editor")) .as_markup(), ) return @@ -330,7 +330,7 @@ async def sync_cluster_handler(callback_query: types.CallbackQuery, session: Any await callback_query.message.answer( f"✅ Ключи успешно синхронизированы для кластера {cluster_name}.", reply_markup=InlineKeyboardBuilder() - .row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor")) + .row(InlineKeyboardButton(text="⬅️ Назад", callback_data="servers_editor")) .as_markup(), ) except Exception as e: @@ -338,7 +338,7 @@ async def sync_cluster_handler(callback_query: types.CallbackQuery, session: Any await callback_query.message.answer( f"❌ Произошла ошибка при синхронизации: {e}", reply_markup=InlineKeyboardBuilder() - .row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor")) + .row(InlineKeyboardButton(text="⬅️ Назад", callback_data="servers_editor")) .as_markup(), ) @@ -374,7 +374,7 @@ async def handle_check_server_availability(callback_query: types.CallbackQuery, availability_message += f"❌ {server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n" builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"manage_cluster|{cluster_name}")) await in_progress_message.edit_text(availability_message, reply_markup=builder.as_markup()) @@ -402,7 +402,7 @@ async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMCo builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"delete_server|{server_name}")) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"manage_cluster|{cluster_name}")) await callback_query.message.answer( f"🔧 Информация о сервере {server_name}:\n\n" @@ -438,7 +438,7 @@ async def handle_confirm_delete_server(callback_query: types.CallbackQuery, stat await delete_server(server_name, session) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад в управление кластерами", callback_data="servers_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад в управление кластерами", callback_data="servers_editor")) await callback_query.message.answer(f"🗑️ Сервер {server_name} успешно удален.", reply_markup=builder.as_markup()) @@ -478,7 +478,7 @@ async def handle_backup_cluster(callback_query: types.CallbackQuery, session: An await create_backup_and_send_to_admins(xui) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="servers_editor")) await callback_query.message.answer( f"Бэкап для кластера {cluster_name} был успешно создан и отправлен администраторам!\n\n" diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index 6deb2aa1..2d470b56 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -44,7 +44,7 @@ class UserEditorState(StatesGroup): @router.callback_query(F.data == "search_by_tg_id", IsAdminFilter()) async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext): builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) await callback_query.message.answer("🔍 Введите Telegram ID клиента:", reply_markup=builder.as_markup()) await state.set_state(UserEditorState.waiting_for_tg_id) @@ -52,7 +52,7 @@ async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext): @router.callback_query(F.data == "search_by_username", IsAdminFilter()) async def prompt_username(callback_query: CallbackQuery, state: FSMContext): builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) await callback_query.message.answer("🔍 Введите Username клиента:", reply_markup=builder.as_markup()) await state.set_state(UserEditorState.waiting_for_username) @@ -64,7 +64,7 @@ async def handle_username_input(message: types.Message, state: FSMContext, sessi if not user_record: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) await message.answer( "🔍 Пользователь с указанным username не найден. 🚫", reply_markup=builder.as_markup(), @@ -80,7 +80,7 @@ async def handle_username_input(message: types.Message, state: FSMContext, sessi if balance is None: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) await message.answer( "🚫 Пользователь с указанным tg_id не найден. 🔍", reply_markup=builder.as_markup(), @@ -109,7 +109,7 @@ async def handle_username_input(message: types.Message, state: FSMContext, sessi builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}")) builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}")) builder.row(InlineKeyboardButton(text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}")) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) user_info = ( f"📊 Информация о пользователе:\n\n" @@ -160,7 +160,7 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext, session: if balance is None: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) await message.answer( "❌ Пользователь с указанным tg_id не найден. 🔍", reply_markup=builder.as_markup(), @@ -189,7 +189,7 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext, session: ) builder.row(InlineKeyboardButton(text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}")) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) user_info = ( f"📊 Информация о пользователе:\n\n" @@ -210,7 +210,7 @@ async def handle_restore_trial(callback_query: types.CallbackQuery, session: Any await update_trial(tg_id, 0, session) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад в меню администратора", callback_data="admin")) + builder.row(InlineKeyboardButton(text="⬅️ Назад в меню администратора", callback_data="admin")) await callback_query.message.answer("✅ Триал успешно восстановлен.", reply_markup=builder.as_markup()) @@ -220,7 +220,7 @@ async def process_balance_change(callback_query: CallbackQuery, state: FSMContex tg_id = int(callback_query.data.split("_")[2]) await state.update_data(tg_id=tg_id) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) await callback_query.message.answer("💸 Введите новую сумму баланса:", reply_markup=builder.as_markup()) await state.set_state(UserEditorState.waiting_for_new_balance) @@ -229,7 +229,7 @@ async def process_balance_change(callback_query: CallbackQuery, state: FSMContex async def handle_new_balance_input(message: types.Message, state: FSMContext, session: Any): if not message.text.isdigit() or int(message.text) < 0: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) await message.answer( "❌ Пожалуйста, введите корректную сумму для изменения баланса.", reply_markup=builder.as_markup(), @@ -251,7 +251,7 @@ async def handle_new_balance_input(message: types.Message, state: FSMContext, se builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( - text="🔙 Назад в меню администратора", + text="⬅️ Назад в меню администратора", callback_data="admin", ) ) @@ -266,7 +266,7 @@ async def process_key_edit(callback_query: CallbackQuery, session: Any): if not key_details: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) await callback_query.message.answer( "🔍 Информация о ключе не найдена. 🚫", reply_markup=builder.as_markup(), @@ -299,7 +299,7 @@ async def process_key_edit(callback_query: CallbackQuery, session: Any): callback_data=f"delete_key_admin|{email}", ) ) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) @@ -307,7 +307,7 @@ async def process_key_edit(callback_query: CallbackQuery, session: Any): @router.callback_query(F.data == "search_by_key_name", IsAdminFilter()) async def prompt_key_name(callback_query: CallbackQuery, state: FSMContext): builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) await callback_query.message.answer("🔑 Введите имя ключа:", reply_markup=builder.as_markup()) await state.set_state(UserEditorState.waiting_for_key_name) @@ -319,7 +319,7 @@ async def handle_key_name_input(message: types.Message, state: FSMContext, sessi if not key_details: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) await message.answer( "🚫 Пользователь с указанным именем ключа не найден.", reply_markup=builder.as_markup(), @@ -353,7 +353,7 @@ async def handle_key_name_input(message: types.Message, state: FSMContext, sessi callback_data=f"delete_key_admin|{key_name}", ) ) - key_buttons.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + key_buttons.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) await message.answer(response_message, reply_markup=key_buttons.as_markup()) await state.clear() @@ -376,7 +376,7 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se if not email: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) await message.answer("📧 Email не найден в состоянии. 🚫", reply_markup=builder.as_markup()) await state.clear() return @@ -391,7 +391,7 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se client_id = await get_client_id_by_email(email) if client_id is None: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) await message.answer( f"🚫 Клиент с email {email} не найден. 🔍", reply_markup=builder.as_markup(), @@ -402,7 +402,7 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se record = await get_key_details(email, session) if not record: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) await message.answer( "🚫 Клиент не найден в базе данных. 🔍", reply_markup=builder.as_markup(), @@ -438,11 +438,11 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se ) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="admin")) await message.answer(response_message, reply_markup=builder.as_markup()) except ValueError: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) await message.answer( "❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.", reply_markup=builder.as_markup(), @@ -459,7 +459,7 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery, sessi if key_details is None: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) await callback_query.message.answer("🔍 Ключ не найден. 🚫", reply_markup=builder.as_markup()) return @@ -526,7 +526,7 @@ async def handle_user_info(callback_query: types.CallbackQuery, state: FSMContex builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}")) builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}")) builder.row(InlineKeyboardButton(text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}")) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) user_info = ( f"📊 Информация о пользователе:\n\n" @@ -581,7 +581,7 @@ async def delete_user(callback_query: types.CallbackQuery, session: Any): try: await delete_user_data(session, tg_id) - back_button = InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor") + back_button = InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor") keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]]) await callback_query.message.answer(f"🗑️ Пользователь с ID {tg_id} был удален.", reply_markup=keyboard) diff --git a/handlers/buttons/gifts.py b/handlers/buttons/gifts.py index f80d956a..3f9df975 100644 --- a/handlers/buttons/gifts.py +++ b/handlers/buttons/gifts.py @@ -1,6 +1,6 @@ GIFT = "🎁 Подарить подписку" MY_GIFTS = "🎁 Мои подарки" PROFILE = "👤 Личный кабинет" -BACK = "🔙 Назад" +BACK = "⬅️ Назад" GIFTS_ABOUT = "Дарите подарки и следите, чтобы они дошли до адресата! 🎄" SHARE_GIFT = "🎁 Поделиться подарком" diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 946689de..8264ee79 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -193,8 +193,8 @@ async def create_key( builder.row(InlineKeyboardButton(text=country, callback_data=callback_data)) logger.info(f"[Country Selection] Добавлена кнопка для страны: {country} с callback_data: {callback_data}") - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="profile")) - logger.info("[Country Selection] Добавлена кнопка '🔙 Назад'.") + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="profile")) + logger.info("[Country Selection] Добавлена кнопка '⬅️ Назад'.") if isinstance(message_or_query, Message): logger.info("[Country Selection] Сообщение пользователя - тип Message.") diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index bece4e4f..753b0e2b 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -218,7 +218,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session ) else: builder.row(InlineKeyboardButton(text="⏳ Продлить", callback_data=f"renew_key|{key_name}")) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="view_keys")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys")) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) keyboard = builder.as_markup() @@ -343,7 +343,7 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio ) ) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="view_keys")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys")) balance = await get_balance(tg_id) diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index 59f6febe..5daeee61 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -214,7 +214,7 @@ async def process_custom_amount_selection(callback_query: types.CallbackQuery, s logger.info(f"User {tg_id} chose to enter a custom amount.") builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="pay_robokassa")) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay_robokassa")) await callback_query.message.answer( "Пожалуйста, введите сумму пополнения.", From 873b178151351b6e7540bbf09dd29da944080f44 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 23 Jan 2025 14:43:39 +0300 Subject: [PATCH 38/49] Add inline button for user profile in admin panel message processing. Enhanced user interaction by including a button that directs users to their personal account, improving the overall user experience. --- handlers/admin/admin_panel.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index e3cfb0ba..96686fa1 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -347,7 +347,10 @@ async def process_message_to_all(message: types.Message, state: FSMContext, sess for record in tg_ids: tg_id = record["tg_id"] try: - await bot.send_message(chat_id=tg_id, text=text_message) + # Создаем кнопку для личного кабинета + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + await bot.send_message(chat_id=tg_id, text=text_message, reply_markup=builder.as_markup()) success_count += 1 except Exception as e: error_count += 1 From 5f2b40b2d2bbb41f02d7ae91f6f68982663759f8 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 29 Jan 2025 04:01:20 +0000 Subject: [PATCH 39/49] Auto-format code with Ruff using pyproject.toml --- backup.py | 11 +- handlers/admin/admin_backups.py | 12 +- handlers/admin/admin_bans.py | 27 +- handlers/admin/admin_coupons.py | 47 +-- handlers/admin/admin_panel.py | 40 +-- handlers/admin/admin_restart.py | 15 +- handlers/admin/admin_sender.py | 40 +-- handlers/admin/admin_servers.py | 187 ++++-------- handlers/admin/admin_stats.py | 52 +--- handlers/admin/admin_user_editor.py | 1 - handlers/admin/admin_users.py | 448 ++++++++-------------------- handlers/keys/key_utils.py | 1 - handlers/keys/keys.py | 13 +- keyboards/admin/bans_kb.py | 14 +- keyboards/admin/coupons_kb.py | 22 +- keyboards/admin/panel_kb.py | 72 +---- keyboards/admin/sender_kb.py | 25 +- keyboards/admin/servers_kb.py | 62 +--- keyboards/admin/stats_kb.py | 14 +- keyboards/admin/users_kb.py | 180 ++--------- middlewares/delete.py | 17 +- utils/csv_export.py | 10 +- 22 files changed, 316 insertions(+), 994 deletions(-) diff --git a/backup.py b/backup.py index 708595c5..196e024a 100644 --- a/backup.py +++ b/backup.py @@ -99,17 +99,12 @@ async def create_backup_and_send_to_admins(xui) -> None: async def _send_backup_to_admins(backup_file_path: str) -> None: try: from bot import bot + with open(backup_file_path, "rb") as backup_file: - backup_input_file = BufferedInputFile( - file=backup_file.read(), - filename=os.path.basename(backup_file_path) - ) + backup_input_file = BufferedInputFile(file=backup_file.read(), filename=os.path.basename(backup_file_path)) admin_ids = ADMIN_ID if isinstance(ADMIN_ID, list) else [ADMIN_ID] for admin_id in admin_ids: - await bot.send_document( - chat_id=admin_id, - document=backup_input_file - ) + await bot.send_document(chat_id=admin_id, document=backup_input_file) logger.info(f"Бэкап базы данных отправлен админу: {admin_id}") except Exception as e: logger.error(f"Ошибка при отправке бэкапа в Telegram: {e}") diff --git a/handlers/admin/admin_backups.py b/handlers/admin/admin_backups.py index 51eee005..e838b891 100644 --- a/handlers/admin/admin_backups.py +++ b/handlers/admin/admin_backups.py @@ -12,14 +12,11 @@ router = Router() AdminPanelCallback.filter(F.action == "backups"), IsAdminFilter(), ) -async def handle_backups( - callback_query: CallbackQuery -): +async def handle_backups(callback_query: CallbackQuery): kb = build_admin_back_kb("management") await callback_query.message.edit_text( - text="💾 Инициализация резервного копирования базы данных...", - reply_markup=kb + text="💾 Инициализация резервного копирования базы данных...", reply_markup=kb ) exception = await backup_database() @@ -29,7 +26,4 @@ async def handle_backups( else: text = "✅ Резервная копия успешно создана и отправлена администраторам." - await callback_query.message.edit_text( - text=text, - reply_markup=kb - ) + await callback_query.message.edit_text(text=text, reply_markup=kb) diff --git a/handlers/admin/admin_bans.py b/handlers/admin/admin_bans.py index a515a14e..9ab8e7cf 100644 --- a/handlers/admin/admin_bans.py +++ b/handlers/admin/admin_bans.py @@ -15,13 +15,8 @@ router = Router() AdminPanelCallback.filter(F.action == "bans"), IsAdminFilter(), ) -async def handle_bans( - callback_query: CallbackQuery -): - text = ( - "🚫 Заблокировавшие бота" - "\n\nЗдесь можно просматривать и удалять пользователей, которые забанили вашего бота!" - ) +async def handle_bans(callback_query: CallbackQuery): + text = "🚫 Заблокировавшие бота\n\nЗдесь можно просматривать и удалять пользователей, которые забанили вашего бота!" await callback_query.message.edit_text( text=text, @@ -33,10 +28,7 @@ async def handle_bans( AdminPanelCallback.filter(F.action == "bans_export"), IsAdminFilter(), ) -async def handle_bans_export( - callback_query: CallbackQuery, - session: Any -): +async def handle_bans_export(callback_query: CallbackQuery, session: Any): kb = build_admin_back_kb("management") try: @@ -53,9 +45,7 @@ async def handle_bans_export( csv_output.seek(0) - document = BufferedInputFile( - file=csv_output.getvalue().encode("utf-8"), filename="banned_users.csv" - ) + document = BufferedInputFile(file=csv_output.getvalue().encode("utf-8"), filename="banned_users.csv") await callback_query.message.answer_document( document=document, @@ -72,10 +62,7 @@ async def handle_bans_export( AdminPanelCallback.filter(F.action == "bans_delete_banned"), IsAdminFilter(), ) -async def handle_bans_delete_banned( - callback_query: CallbackQuery, - session: Any -): +async def handle_bans_delete_banned(callback_query: CallbackQuery, session: Any): kb = build_admin_back_kb("bans") try: @@ -92,9 +79,7 @@ async def handle_bans_delete_banned( for tg_id in blocked_ids: await delete_user_data(session, tg_id) - await session.execute( - "DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids - ) + await session.execute("DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids) await callback_query.message.answer( text=f"🗑️ Удалены данные о {len(blocked_ids)} пользователях и связанных записях.", diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index ba930fa0..b65ea5e4 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -22,22 +22,16 @@ class AdminCouponsState(StatesGroup): IsAdminFilter(), ) async def handle_coupons( - callback_query: types.CallbackQuery, + callback_query: types.CallbackQuery, ): - await callback_query.message.edit_text( - text="🛠 Меню управления купонами:", - reply_markup=build_coupons_kb() - ) + await callback_query.message.edit_text(text="🛠 Меню управления купонами:", reply_markup=build_coupons_kb()) @router.callback_query( AdminPanelCallback.filter(F.action == "coupons_create"), IsAdminFilter(), ) -async def handle_coupons_create( - callback_query: types.CallbackQuery, - state: FSMContext -): +async def handle_coupons_create(callback_query: types.CallbackQuery, state: FSMContext): text = ( "🎫 Введите данные для создания купона в формате:\n\n" "📝 код 💰 сумма 🔢 лимит\n\n" @@ -51,15 +45,8 @@ async def handle_coupons_create( await state.set_state(AdminCouponsState.waiting_for_coupon_data) -@router.message( - AdminCouponsState.waiting_for_coupon_data, - IsAdminFilter() -) -async def handle_coupon_data_input( - message: types.Message, - state: FSMContext, - session: Any -): +@router.message(AdminCouponsState.waiting_for_coupon_data, IsAdminFilter()) +async def handle_coupon_data_input(message: types.Message, state: FSMContext, session: Any): text = message.text.strip() parts = text.split() @@ -83,10 +70,7 @@ async def handle_coupon_data_input( coupon_amount = float(parts[1]) usage_limit = int(parts[2]) except ValueError: - text = ( - "⚠️ Проверьте правильность введенных данных!\n" - "💱 Сумма должна быть числом, а лимит — целым числом." - ) + text = "⚠️ Проверьте правильность введенных данных!\n💱 Сумма должна быть числом, а лимит — целым числом." await message.answer( text=text, @@ -103,10 +87,7 @@ async def handle_coupon_data_input( f"🔢 Лимит использования: {usage_limit} раз" ) - await message.answer( - text=text, - reply_markup=kb - ) + await message.answer(text=text, reply_markup=kb) await state.clear() except Exception as e: @@ -117,10 +98,7 @@ async def handle_coupon_data_input( AdminPanelCallback.filter(F.action == "coupons_list"), IsAdminFilter(), ) -async def handle_coupons_list( - callback_query: types.CallbackQuery, - session: Any -): +async def handle_coupons_list(callback_query: types.CallbackQuery, session: Any): try: page = int(callback_query.data.split(":")[1]) if ":" in callback_query.data else 1 per_page = 10 @@ -147,10 +125,7 @@ async def handle_coupons_list( f"✅ Использовано: {coupon['usage_count']} раз\n\n" ) - await callback_query.message.edit_text( - text=coupon_list, - reply_markup=kb - ) + await callback_query.message.edit_text(text=coupon_list, reply_markup=kb) except Exception as e: logger.error(f"Ошибка при получении списка купонов: {e}") @@ -162,9 +137,7 @@ async def handle_coupons_list( IsAdminFilter(), ) async def handle_coupon_delete( - callback_query: types.CallbackQuery, - callback_data: AdminCouponDeleteCallback, - session: Any + callback_query: types.CallbackQuery, callback_data: AdminCouponDeleteCallback, session: Any ): coupon_code = callback_data.coupon_code diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index a5594caa..eb9eb744 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -10,52 +10,28 @@ from keyboards.admin.panel_kb import build_panel_kb, AdminPanelCallback, build_m router = Router() -@router.callback_query( - AdminPanelCallback.filter(F.action == "admin"), - IsAdminFilter() -) +@router.callback_query(AdminPanelCallback.filter(F.action == "admin"), IsAdminFilter()) async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMContext): - text = ( - "🤖 Панель администратора" - f"\n📌 Версия бота: {version}" - ) + text = f"🤖 Панель администратора\n📌 Версия бота: {version}" await state.clear() - await callback_query.message.edit_text( - text=text, - reply_markup=build_panel_kb() - ) + await callback_query.message.edit_text(text=text, reply_markup=build_panel_kb()) -@router.callback_query( - F.data == "admin", - IsAdminFilter() -) +@router.callback_query(F.data == "admin", IsAdminFilter()) async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMContext): await handle_admin_message(callback_query.message, state) -@router.message( - Command("admin"), - IsAdminFilter() -) +@router.message(Command("admin"), IsAdminFilter()) async def handle_admin_message(message: types.Message, state: FSMContext): - text = ( - "🤖 Панель администратора" - f"\n📌 Версия бота: {version}" - ) + text = f"🤖 Панель администратора\n📌 Версия бота: {version}" await state.clear() - await message.answer( - text=text, - reply_markup=build_panel_kb() - ) + await message.answer(text=text, reply_markup=build_panel_kb()) -@router.callback_query( - AdminPanelCallback.filter(F.action == "management"), - IsAdminFilter() -) +@router.callback_query(AdminPanelCallback.filter(F.action == "management"), IsAdminFilter()) async def handle_management(callback_query: CallbackQuery): await callback_query.message.edit_text( text="🤖 Управление ботом", diff --git a/handlers/admin/admin_restart.py b/handlers/admin/admin_restart.py index 157bf13b..58c1a115 100644 --- a/handlers/admin/admin_restart.py +++ b/handlers/admin/admin_restart.py @@ -33,17 +33,8 @@ async def handle_restart_confirm(callback_query: CallbackQuery): capture_output=True, text=True, ) - await callback_query.message.edit_text( - text="🔄 Бот успешно перезагружен!", - reply_markup=kb - ) + await callback_query.message.edit_text(text="🔄 Бот успешно перезагружен!", reply_markup=kb) except subprocess.CalledProcessError: - await callback_query.message.edit_text( - text="🔄 Бот успешно перезагружен!", - reply_markup=kb - ) + await callback_query.message.edit_text(text="🔄 Бот успешно перезагружен!", reply_markup=kb) except Exception as e: - await callback_query.message.edit_text( - text=f"⚠️ Ошибка при перезагрузке бота: {e.stderr}", - reply_markup=kb - ) + await callback_query.message.edit_text(text=f"⚠️ Ошибка при перезагрузке бота: {e.stderr}", reply_markup=kb) diff --git a/handlers/admin/admin_sender.py b/handlers/admin/admin_sender.py index e82598bd..70563437 100644 --- a/handlers/admin/admin_sender.py +++ b/handlers/admin/admin_sender.py @@ -22,9 +22,7 @@ class AdminSender(StatesGroup): AdminPanelCallback.filter(F.action == "sender"), IsAdminFilter(), ) -async def handle_sender( - callback_query: CallbackQuery -): +async def handle_sender(callback_query: CallbackQuery): await callback_query.message.edit_text( text="✍️ Выберите группу пользователей для рассылки:", reply_markup=build_sender_kb(), @@ -35,11 +33,7 @@ async def handle_sender( AdminSenderCallback.filter(), IsAdminFilter(), ) -async def handle_sender_callback( - callback_query: CallbackQuery, - callback_data: AdminSenderCallback, - state: FSMContext -): +async def handle_sender_callback(callback_query: CallbackQuery, callback_data: AdminSenderCallback, state: FSMContext): await callback_query.message.edit_text( text="✍️ Введите текст сообщения для рассылки:", reply_markup=build_admin_back_kb("sender"), @@ -52,11 +46,7 @@ async def handle_sender_callback( AdminSender.waiting_for_message, IsAdminFilter(), ) -async def handle_message_input( - message: types.Message, - state: FSMContext, - session: Any -): +async def handle_message_input(message: types.Message, state: FSMContext, session: Any): text_message = message.text try: @@ -64,20 +54,26 @@ async def handle_message_input( send_to = state_data.get("type", "all") if send_to == "subscribed": - tg_ids = await session.fetch(""" + tg_ids = await session.fetch( + """ SELECT DISTINCT c.tg_id FROM connections c JOIN keys k ON c.tg_id = k.tg_id WHERE k.expiry_time > $1 - """, int(datetime.utcnow().timestamp() * 1000)) + """, + int(datetime.utcnow().timestamp() * 1000), + ) elif send_to == "unsubscribed": - tg_ids = await session.fetch(""" + tg_ids = await session.fetch( + """ SELECT c.tg_id FROM connections c LEFT JOIN keys k ON c.tg_id = k.tg_id GROUP BY c.tg_id HAVING COUNT(k.tg_id) = 0 OR MAX(k.expiry_time) <= $1 - """, int(datetime.utcnow().timestamp() * 1000)) + """, + int(datetime.utcnow().timestamp() * 1000), + ) else: tg_ids = await session.fetch("SELECT DISTINCT tg_id FROM connections") @@ -87,10 +83,7 @@ async def handle_message_input( for record in tg_ids: tg_id = record["tg_id"] try: - await message.bot.send_message( - chat_id=tg_id, - text=text_message - ) + await message.bot.send_message(chat_id=tg_id, text=text_message) success_count += 1 except Exception: pass @@ -102,10 +95,7 @@ async def handle_message_input( f"\n❌ Не доставлено: {total_users - success_count}" ) - await message.answer( - text=text, - reply_markup=build_admin_back_kb("stats") - ) + await message.answer(text=text, reply_markup=build_admin_back_kb("stats")) except Exception as e: logger.error(f"❗ Ошибка при подключении к базе данных: {e}") diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index 84675bfc..51257e9a 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -9,9 +9,13 @@ from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL from database import create_server, check_unique_server_name, delete_server, get_keys_by_server, get_servers from filters.admin import IsAdminFilter from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb -from keyboards.admin.servers_kb import build_manage_server_kb, \ - build_delete_server_kb, \ - build_manage_cluster_kb, build_clusters_editor_kb, AdminServerEditorCallback +from keyboards.admin.servers_kb import ( + build_manage_server_kb, + build_delete_server_kb, + build_manage_cluster_kb, + build_clusters_editor_kb, + AdminServerEditorCallback, +) router = Router() @@ -28,9 +32,7 @@ class AdminServersEditor(StatesGroup): AdminPanelCallback.filter(F.action == "servers"), IsAdminFilter(), ) -async def handle_servers( - callback_query: types.CallbackQuery -): +async def handle_servers(callback_query: types.CallbackQuery): servers = await get_servers_from_db() text = ( @@ -51,36 +53,23 @@ async def handle_servers( AdminPanelCallback.filter(F.action == "clusters_add"), IsAdminFilter(), ) -async def handle_clusters_add( - callback_query: types.CallbackQuery, - state: FSMContext -): +async def handle_clusters_add(callback_query: types.CallbackQuery, state: FSMContext): text = ( "🔧 Введите имя нового кластера:\n\n" "Имя кластера должно быть уникальным!\n" "Пример: cluster1 или us_east_1" ) - await callback_query.message.edit_text( - text=text, - reply_markup=build_admin_back_kb("servers") - ) + await callback_query.message.edit_text(text=text, reply_markup=build_admin_back_kb("servers")) await state.set_state(AdminServersEditor.waiting_for_cluster_name) -@router.message( - AdminServersEditor.waiting_for_cluster_name, - IsAdminFilter() -) -async def handle_cluster_name_input( - message: types.Message, - state: FSMContext -): +@router.message(AdminServersEditor.waiting_for_cluster_name, IsAdminFilter()) +async def handle_cluster_name_input(message: types.Message, state: FSMContext): if not message.text: await message.answer( - text="❌ Имя кластера не может быть пустым. Попробуйте снова.", - reply_markup=build_admin_back_kb("servers") + text="❌ Имя кластера не может быть пустым. Попробуйте снова.", reply_markup=build_admin_back_kb("servers") ) return @@ -101,18 +90,11 @@ async def handle_cluster_name_input( await state.set_state(AdminServersEditor.waiting_for_server_name) -@router.message( - AdminServersEditor.waiting_for_server_name, - IsAdminFilter() -) -async def handle_server_name_input( - message: types.Message, - state: FSMContext -): +@router.message(AdminServersEditor.waiting_for_server_name, IsAdminFilter()) +async def handle_server_name_input(message: types.Message, state: FSMContext): if not message.text: await message.answer( - text="❌ Имя сервера не может быть пустым. Попробуйте снова.", - reply_markup=build_admin_back_kb("servers") + text="❌ Имя сервера не может быть пустым. Попробуйте снова.", reply_markup=build_admin_back_kb("servers") ) return @@ -121,7 +103,7 @@ async def handle_server_name_input( if not await check_unique_server_name(server_name): await message.answer( text="❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя.", - reply_markup=build_admin_back_kb("servers") + reply_markup=build_admin_back_kb("servers"), ) return @@ -144,18 +126,12 @@ async def handle_server_name_input( await state.set_state(AdminServersEditor.waiting_for_api_url) -@router.message( - AdminServersEditor.waiting_for_api_url, - IsAdminFilter() -) -async def handle_api_url_input( - message: types.Message, - state: FSMContext -): +@router.message(AdminServersEditor.waiting_for_api_url, IsAdminFilter()) +async def handle_api_url_input(message: types.Message, state: FSMContext): if not message.text or not message.text.strip().startswith("https://"): await message.answer( text="❌ API URL должен начинаться с https://. Попробуйте снова.", - reply_markup=build_admin_back_kb("servers") + reply_markup=build_admin_back_kb("servers"), ) return @@ -182,18 +158,12 @@ async def handle_api_url_input( await state.set_state(AdminServersEditor.waiting_for_subscription_url) -@router.message( - AdminServersEditor.waiting_for_subscription_url, - IsAdminFilter() -) -async def handle_subscription_url_input( - message: types.Message, - state: FSMContext -): +@router.message(AdminServersEditor.waiting_for_subscription_url, IsAdminFilter()) +async def handle_subscription_url_input(message: types.Message, state: FSMContext): if not message.text or not message.text.strip().startswith("https://"): await message.answer( text="❌ subscription_url должен начинаться с https://. Попробуйте снова.", - reply_markup=build_admin_back_kb("servers") + reply_markup=build_admin_back_kb("servers"), ) return @@ -216,20 +186,14 @@ async def handle_subscription_url_input( await state.set_state(AdminServersEditor.waiting_for_inbound_id) -@router.message( - AdminServersEditor.waiting_for_inbound_id, - IsAdminFilter() -) -async def handle_inbound_id_input( - message: types.Message, - state: FSMContext -): +@router.message(AdminServersEditor.waiting_for_inbound_id, IsAdminFilter()) +async def handle_inbound_id_input(message: types.Message, state: FSMContext): inbound_id = message.text.strip() if not inbound_id.isdigit(): await message.answer( text="❌ inbound_id должен быть числовым значением. Попробуйте снова.", - reply_markup=build_admin_back_kb("servers") + reply_markup=build_admin_back_kb("servers"), ) return @@ -261,13 +225,10 @@ async def handle_inbound_id_input( await state.clear() -@router.callback_query( - AdminServerEditorCallback.filter(F.action == "clusters_manage"), - IsAdminFilter() -) +@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_manage"), IsAdminFilter()) async def handle_clusters_manage( - callback_query: types.CallbackQuery, - callback_data: AdminServerEditorCallback, + callback_query: types.CallbackQuery, + callback_data: AdminServerEditorCallback, ): cluster_name = callback_data.data @@ -280,23 +241,15 @@ async def handle_clusters_manage( ) -@router.callback_query( - AdminServerEditorCallback.filter(F.action == "servers_availability"), - IsAdminFilter() -) -async def handle_servers_availability( - callback_query: types.CallbackQuery, - callback_data: AdminServerEditorCallback -): +@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_availability"), IsAdminFilter()) +async def handle_servers_availability(callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback): cluster_name = callback_data.data servers = await get_servers(session) cluster_servers = servers.get(cluster_name, []) if not cluster_servers: - await callback_query.message.answer( - text=f"Кластер '{cluster_name}' не содержит серверов." - ) + await callback_query.message.answer(text=f"Кластер '{cluster_name}' не содержит серверов.") return text = ( @@ -304,13 +257,9 @@ async def handle_servers_availability( "Это может занять до 1 минуты, пожалуйста, подождите..." ) - in_progress_message = await callback_query.message.answer( - text=text - ) + in_progress_message = await callback_query.message.answer(text=text) - text = ( - f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n" - ) + text = f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n" for server in cluster_servers: xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD) @@ -319,34 +268,21 @@ async def handle_servers_availability( await xui.login() online_users = len(await xui.client.online()) - text += ( - f"🌍 {server['server_name']}: {online_users} активных пользователей.\n" - ) + text += f"🌍 {server['server_name']}: {online_users} активных пользователей.\n" except Exception as e: text += f"❌ {server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n" - await in_progress_message.edit_text( - text=text, - reply_markup=build_admin_back_kb("servers") - ) + await in_progress_message.edit_text(text=text, reply_markup=build_admin_back_kb("servers")) -@router.callback_query( - AdminServerEditorCallback.filter(F.action == "servers_manage"), - IsAdminFilter() -) -async def handle_servers_manage( - callback_query: types.CallbackQuery, - callback_data: AdminServerEditorCallback -): +@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_manage"), IsAdminFilter()) +async def handle_servers_manage(callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback): server_name = callback_data.data servers = await get_servers_from_db() cluster_name, server = next( - ((c, s) for c, cs in servers.items() - for s in cs if s["server_name"] == server_name), - (None, None) + ((c, s) for c, cs in servers.items() for s in cs if s["server_name"] == server_name), (None, None) ) if server: @@ -366,19 +302,11 @@ async def handle_servers_manage( reply_markup=build_manage_server_kb(server_name, cluster_name), ) else: - await callback_query.message.edit_text( - text="❌ Сервер не найден." - ) + await callback_query.message.edit_text(text="❌ Сервер не найден.") -@router.callback_query( - AdminServerEditorCallback.filter(F.action == "servers_delete"), - IsAdminFilter() -) -async def handle_servers_delete( - callback_query: types.CallbackQuery, - callback_data: AdminServerEditorCallback -): +@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_delete"), IsAdminFilter()) +async def handle_servers_delete(callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback): server_name = callback_data.data await callback_query.message.edit_text( @@ -387,32 +315,20 @@ async def handle_servers_delete( ) -@router.callback_query( - AdminServerEditorCallback.filter(F.action == "servers_delete_confirm"), - IsAdminFilter() -) -async def handle_servers_delete_confirm( - callback_query: types.CallbackQuery, - callback_data: AdminServerEditorCallback -): +@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_delete_confirm"), IsAdminFilter()) +async def handle_servers_delete_confirm(callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback): server_name = callback_data.data await delete_server(server_name, session) await callback_query.message.edit_text( - text=f"🗑️ Сервер {server_name} успешно удален.", - reply_markup=build_admin_back_kb("servers") + text=f"🗑️ Сервер {server_name} успешно удален.", reply_markup=build_admin_back_kb("servers") ) -@router.callback_query( - AdminServerEditorCallback.filter(F.action == "servers_add"), - IsAdminFilter() -) +@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_add"), IsAdminFilter()) async def handle_servers_add( - callback_query: types.CallbackQuery, - callback_data: AdminServerEditorCallback, - state: FSMContext + callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, state: FSMContext ): cluster_name = callback_data.data @@ -432,13 +348,10 @@ async def handle_servers_add( await state.set_state(AdminServersEditor.waiting_for_server_name) -@router.callback_query( - AdminServerEditorCallback.filter(F.action == "clusters_backup"), - IsAdminFilter() -) +@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_backup"), IsAdminFilter()) async def handle_clusters_backup( - callback_query: types.CallbackQuery, - callback_data: AdminServerEditorCallback, + callback_query: types.CallbackQuery, + callback_data: AdminServerEditorCallback, ): cluster_name = callback_data.data diff --git a/handlers/admin/admin_stats.py b/handlers/admin/admin_stats.py index 0a74d69f..48ad79bb 100644 --- a/handlers/admin/admin_stats.py +++ b/handlers/admin/admin_stats.py @@ -17,10 +17,7 @@ router = Router() AdminPanelCallback.filter(F.action == "stats"), IsAdminFilter(), ) -async def handle_stats( - callback_query: CallbackQuery, - session: Any -): +async def handle_stats(callback_query: CallbackQuery, session: Any): try: total_users = await session.fetchval("SELECT COUNT(*) FROM users") total_keys = await session.fetchval("SELECT COUNT(*) FROM keys") @@ -35,13 +32,9 @@ async def handle_stats( total_payments_month = await session.fetchval( "SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('month', CURRENT_DATE)" ) - total_payments_all_time = await session.fetchval( - "SELECT COALESCE(SUM(amount), 0) FROM payments" - ) + total_payments_all_time = await session.fetchval("SELECT COALESCE(SUM(amount), 0) FROM payments") - registrations_today = await session.fetchval( - "SELECT COUNT(*) FROM users WHERE created_at >= CURRENT_DATE" - ) + registrations_today = await session.fetchval("SELECT COUNT(*) FROM users WHERE created_at >= CURRENT_DATE") registrations_week = await session.fetchval( "SELECT COUNT(*) FROM users WHERE created_at >= date_trunc('week', CURRENT_DATE)" ) @@ -49,9 +42,7 @@ async def handle_stats( "SELECT COUNT(*) FROM users WHERE created_at >= date_trunc('month', CURRENT_DATE)" ) - users_updated_today = await session.fetchval( - "SELECT COUNT(*) FROM users WHERE updated_at >= CURRENT_DATE" - ) + users_updated_today = await session.fetchval("SELECT COUNT(*) FROM users WHERE updated_at >= CURRENT_DATE") active_keys = await session.fetchval( "SELECT COUNT(*) FROM keys WHERE expiry_time > $1", @@ -81,10 +72,7 @@ async def handle_stats( f" 🏦 За все время: {total_payments_all_time} ₽\n" ) - await callback_query.message.edit_text( - text=stats_message, - reply_markup=build_stats_kb() - ) + await callback_query.message.edit_text(text=stats_message, reply_markup=build_stats_kb()) except Exception as e: logger.error(f"Error in user_stats_menu: {e}") @@ -93,46 +81,28 @@ async def handle_stats( AdminPanelCallback.filter(F.action == "stats_export_users_csv"), IsAdminFilter(), ) -async def handle_export_users_csv( - callback_query: CallbackQuery, - session: Any -): +async def handle_export_users_csv(callback_query: CallbackQuery, session: Any): kb = build_admin_back_kb("stats") try: export = await export_users_csv(session) - await callback_query.message.answer_document( - document=export, - caption="📥 Экспорт пользователей в CSV" - ) + await callback_query.message.answer_document(document=export, caption="📥 Экспорт пользователей в CSV") except Exception as e: logger.error(f"Ошибка при экспорте пользователей в CSV: {e}") - await callback_query.message.edit_text( - text=f"❗ Произошла ошибка при экспорте: {e}", - reply_markup=kb - ) + await callback_query.message.edit_text(text=f"❗ Произошла ошибка при экспорте: {e}", reply_markup=kb) @router.callback_query( AdminPanelCallback.filter(F.action == "stats_export_payments_csv"), IsAdminFilter(), ) -async def handle_export_payments_csv( - callback_query: CallbackQuery, - session: Any -): +async def handle_export_payments_csv(callback_query: CallbackQuery, session: Any): kb = build_admin_back_kb("stats") try: export = await export_payments_csv(session) - await callback_query.message.answer_document( - document=export, - caption="📥 Экспорт платежей в CSV" - ) + await callback_query.message.answer_document(document=export, caption="📥 Экспорт платежей в CSV") except Exception as e: logger.error(f"Ошибка при экспорте платежей в CSV: {e}") - await callback_query.message.edit_text( - text=f"❗ Произошла ошибка при экспорте: {e}", - reply_markup=kb - ) + await callback_query.message.edit_text(text=f"❗ Произошла ошибка при экспорте: {e}", reply_markup=kb) diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index a3aec08a..2d470b56 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -590,4 +590,3 @@ async def delete_user(callback_query: types.CallbackQuery, session: Any): await callback_query.message.answer( f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова." ) - diff --git a/handlers/admin/admin_users.py b/handlers/admin/admin_users.py index 2520299a..4601d646 100644 --- a/handlers/admin/admin_users.py +++ b/handlers/admin/admin_users.py @@ -15,14 +15,25 @@ from filters.admin import IsAdminFilter from handlers.keys.key_utils import ( delete_key_from_cluster, delete_key_from_db, - renew_key_in_cluster, update_subscription, + renew_key_in_cluster, + update_subscription, ) from handlers.utils import sanitize_key_name from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb -from keyboards.admin.users_kb import build_user_edit_kb, build_key_edit_kb, build_key_delete_kb, \ - build_user_delete_kb, AdminUserEditorCallback, build_editor_kb, build_users_balance_kb, \ - build_users_balance_change_kb, build_user_key_kb, build_users_key_expiry_kb, AdminUserKeyEditorCallback, \ - build_users_key_show_kb +from keyboards.admin.users_kb import ( + build_user_edit_kb, + build_key_edit_kb, + build_key_delete_kb, + build_user_delete_kb, + AdminUserEditorCallback, + build_editor_kb, + build_users_balance_kb, + build_users_balance_change_kb, + build_user_key_kb, + build_users_key_expiry_kb, + AdminUserKeyEditorCallback, + build_users_key_show_kb, +) from logger import logger router = Router() @@ -42,10 +53,7 @@ class UserEditorState(StatesGroup): AdminPanelCallback.filter(F.action == "search_user"), IsAdminFilter(), ) -async def handle_search_user( - callback_query: CallbackQuery, - state: FSMContext -): +async def handle_search_user(callback_query: CallbackQuery, state: FSMContext): text = ( "🔍 Поиск пользователя" "\n\n📌 Введите ID, Username или перешлите сообщение пользователя." @@ -55,36 +63,20 @@ async def handle_search_user( ) await state.set_state(UserEditorState.waiting_for_user_data) - await callback_query.message.edit_text( - text=text, - reply_markup=build_admin_back_kb() - ) + await callback_query.message.edit_text(text=text, reply_markup=build_admin_back_kb()) @router.callback_query( AdminPanelCallback.filter(F.action == "search_key"), IsAdminFilter(), ) -async def handle_search_key( - callback_query: CallbackQuery, - state: FSMContext -): +async def handle_search_key(callback_query: CallbackQuery, state: FSMContext): await state.set_state(UserEditorState.waiting_for_key_name) - await callback_query.message.edit_text( - text="🔑 Введите имя ключа для поиска:", - reply_markup=build_admin_back_kb() - ) + await callback_query.message.edit_text(text="🔑 Введите имя ключа для поиска:", reply_markup=build_admin_back_kb()) -@router.message( - UserEditorState.waiting_for_user_data, - IsAdminFilter() -) -async def handle_user_data_input( - message: types.Message, - state: FSMContext, - session: Any -): +@router.message(UserEditorState.waiting_for_user_data, IsAdminFilter()) +async def handle_user_data_input(message: types.Message, state: FSMContext, session: Any): kb = build_admin_back_kb() if message.forward_from: @@ -93,23 +85,18 @@ async def handle_user_data_input( return if not message.text: - await message.answer( - text="🚫 Пожалуйста, отправьте текстовое сообщение.", - reply_markup=kb - ) + await message.answer(text="🚫 Пожалуйста, отправьте текстовое сообщение.", reply_markup=kb) return if message.text.isdigit(): tg_id = int(message.text) else: # Удаление '@' символа в начале сообщения - username = message.text.strip().lstrip('@') + username = message.text.strip().lstrip("@") # Удаление начала ссылки на профиль - username = username.replace('https://t.me/', '') + username = username.replace("https://t.me/", "") - user = await session.fetchrow( - "SELECT tg_id FROM users WHERE username = $1", username - ) + user = await session.fetchrow("SELECT tg_id FROM users WHERE username = $1", username) if not user: await message.answer( @@ -123,32 +110,19 @@ async def handle_user_data_input( await process_user_search(message, state, session, tg_id) -@router.message( - UserEditorState.waiting_for_key_name, - IsAdminFilter() -) -async def handle_key_name_input( - message: types.Message, - state: FSMContext, - session: Any -): +@router.message(UserEditorState.waiting_for_key_name, IsAdminFilter()) +async def handle_key_name_input(message: types.Message, state: FSMContext, session: Any): kb = build_admin_back_kb() if not message.text: - await message.answer( - text="🚫 Пожалуйста, отправьте текстовое сообщение.", - reply_markup=kb - ) + await message.answer(text="🚫 Пожалуйста, отправьте текстовое сообщение.", reply_markup=kb) return key_name = sanitize_key_name(message.text) key_details = await get_key_details(key_name, session) if not key_details: - await message.answer( - text="🚫 Пользователь с указанным именем ключа не найден.", - reply_markup=kb - ) + await message.answer(text="🚫 Пользователь с указанным именем ключа не найден.", reply_markup=kb) return await process_user_search(message, state, session, key_details["tg_id"]) @@ -159,46 +133,28 @@ async def handle_key_name_input( IsAdminFilter(), ) async def handle_send_message( - callback_query: types.CallbackQuery, - callback_data: AdminUserEditorCallback, - state: FSMContext + callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext ): tg_id = callback_data.tg_id await callback_query.message.edit_text( - text="✉️ Введите текст сообщения, которое вы хотите отправить пользователю:", - reply_markup=build_editor_kb(tg_id) + text="✉️ Введите текст сообщения, которое вы хотите отправить пользователю:", reply_markup=build_editor_kb(tg_id) ) await state.update_data(tg_id=tg_id) await state.set_state(UserEditorState.waiting_for_message_text) -@router.message( - UserEditorState.waiting_for_message_text, - IsAdminFilter() -) -async def handle_message_text_input( - message: types.Message, - state: FSMContext -): +@router.message(UserEditorState.waiting_for_message_text, IsAdminFilter()) +async def handle_message_text_input(message: types.Message, state: FSMContext): data = await state.get_data() tg_id = data.get("tg_id") try: - await message.bot.send_message( - chat_id=tg_id, - text=message.text - ) - await message.answer( - text="✅ Сообщение успешно отправлено.", - reply_markup=build_editor_kb(tg_id) - ) + await message.bot.send_message(chat_id=tg_id, text=message.text) + await message.answer(text="✅ Сообщение успешно отправлено.", reply_markup=build_editor_kb(tg_id)) except Exception as e: - await message.answer( - text=f"❌ Не удалось отправить сообщение: {e}", - reply_markup=build_editor_kb(tg_id) - ) + await message.answer(text=f"❌ Не удалось отправить сообщение: {e}", reply_markup=build_editor_kb(tg_id)) await state.clear() @@ -208,37 +164,28 @@ async def handle_message_text_input( IsAdminFilter(), ) async def handle_trial_restore( - callback_query: types.CallbackQuery, - callback_data: AdminUserEditorCallback, - session: Any + callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any ): tg_id = callback_data.tg_id await restore_trial(tg_id, session) - await callback_query.message.edit_text( - text="✅ Триал успешно восстановлен!", - reply_markup=build_editor_kb(tg_id) - ) + await callback_query.message.edit_text(text="✅ Триал успешно восстановлен!", reply_markup=build_editor_kb(tg_id)) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_balance_edit"), - IsAdminFilter() -) -async def handle_balance_change( - callback_query: CallbackQuery, - callback_data: AdminUserEditorCallback, - session: Any -): +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_edit"), IsAdminFilter()) +async def handle_balance_change(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any): tg_id = callback_data.tg_id - records = await session.fetch(""" + records = await session.fetch( + """ SELECT amount, payment_system, status, created_at FROM payments WHERE tg_id = $1 ORDER BY created_at DESC LIMIT 5 - """, tg_id) + """, + tg_id, + ) balance = await get_user_balance(tg_id, session) @@ -263,21 +210,12 @@ async def handle_balance_change( else: text += "\n 🚫 Отсутствуют" - await callback_query.message.edit_text( - text=text, - reply_markup=build_users_balance_kb(tg_id) - ) + await callback_query.message.edit_text(text=text, reply_markup=build_users_balance_kb(tg_id)) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_balance_add"), - IsAdminFilter() -) +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_add"), IsAdminFilter()) async def handle_balance_add( - callback_query: CallbackQuery, - callback_data: AdminUserEditorCallback, - state: FSMContext, - session: Any + callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any ): tg_id = callback_data.tg_id amount = callback_data.data @@ -292,19 +230,12 @@ async def handle_balance_add( await callback_query.message.edit_text( text="✍️ Введите сумму, которую хотите добавить на баланс пользователя:", - reply_markup=build_users_balance_change_kb(tg_id) + reply_markup=build_users_balance_change_kb(tg_id), ) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_balance_take"), - IsAdminFilter() -) -async def handle_balance_take( - callback_query: CallbackQuery, - callback_data: AdminUserEditorCallback, - state: FSMContext -): +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_take"), IsAdminFilter()) +async def handle_balance_take(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext): tg_id = callback_data.tg_id await state.update_data(tg_id=tg_id, op_type="take") @@ -312,19 +243,12 @@ async def handle_balance_take( await callback_query.message.edit_text( text="✍️ Введите сумму, которую хотите вычесть из баланса пользователя:", - reply_markup=build_users_balance_change_kb(tg_id) + reply_markup=build_users_balance_change_kb(tg_id), ) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_balance_set"), - IsAdminFilter() -) -async def handle_balance_set( - callback_query: CallbackQuery, - callback_data: AdminUserEditorCallback, - state: FSMContext -): +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_set"), IsAdminFilter()) +async def handle_balance_set(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext): tg_id = callback_data.tg_id await state.update_data(tg_id=tg_id, op_type="set") @@ -332,27 +256,19 @@ async def handle_balance_set( await callback_query.message.edit_text( text="✍️ Введите баланс, который хотите установить пользователю:", - reply_markup=build_users_balance_change_kb(tg_id) + reply_markup=build_users_balance_change_kb(tg_id), ) -@router.message( - UserEditorState.waiting_for_balance, - IsAdminFilter() -) -async def handle_balance_input( - message: types.Message, - state: FSMContext, - session: Any -): +@router.message(UserEditorState.waiting_for_balance, IsAdminFilter()) +async def handle_balance_input(message: types.Message, state: FSMContext, session: Any): data = await state.get_data() tg_id = data.get("tg_id") op_type = data.get("op_type") if not message.text.isdigit() or int(message.text) < 0: await message.answer( - text="🚫 Пожалуйста, введите корректную сумму!", - reply_markup=build_users_balance_change_kb(tg_id) + text="🚫 Пожалуйста, введите корректную сумму!", reply_markup=build_users_balance_change_kb(tg_id) ) return @@ -368,21 +284,12 @@ async def handle_balance_input( text = f"✅ Баланс пользователя изменен на {amount}Р" await set_user_balance(tg_id, amount, session) - await message.answer( - text=text, - reply_markup=build_users_balance_change_kb(tg_id) - ) + await message.answer(text=text, reply_markup=build_users_balance_change_kb(tg_id)) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_key_edit"), - IsAdminFilter() -) +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_key_edit"), IsAdminFilter()) async def handle_key_edit( - callback_query: CallbackQuery, - callback_data: CallbackData, - session: Any, - update: bool = False + callback_query: CallbackQuery, callback_data: CallbackData, session: Any, update: bool = False ): email = callback_data.data key_details = await get_key_details(email, session) @@ -403,42 +310,24 @@ async def handle_key_edit( ) if not update or not callback_data.edit: - await callback_query.message.edit_text( - text=text, - reply_markup=build_key_edit_kb(key_details, email) - ) + await callback_query.message.edit_text(text=text, reply_markup=build_key_edit_kb(key_details, email)) else: await callback_query.message.edit_text( - text=text, - reply_markup=build_users_key_expiry_kb(callback_data.tg_id, email) + text=text, reply_markup=build_users_key_expiry_kb(callback_data.tg_id, email) ) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_expiry_edit"), - IsAdminFilter() -) -async def handle_change_expiry( - callback_query: CallbackQuery, - callback_data: AdminUserEditorCallback -): +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_expiry_edit"), IsAdminFilter()) +async def handle_change_expiry(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback): tg_id = callback_data.tg_id email = callback_data.data - await callback_query.message.edit_reply_markup( - reply_markup=build_users_key_expiry_kb(tg_id, email) - ) + await callback_query.message.edit_reply_markup(reply_markup=build_users_key_expiry_kb(tg_id, email)) -@router.callback_query( - AdminUserKeyEditorCallback.filter(F.action == "add"), - IsAdminFilter() -) +@router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "add"), IsAdminFilter()) async def handle_expiry_add( - callback_query: CallbackQuery, - callback_data: AdminUserKeyEditorCallback, - state: FSMContext, - session: Any + callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext, session: Any ): tg_id = callback_data.tg_id email = callback_data.data @@ -463,18 +352,13 @@ async def handle_expiry_add( await callback_query.message.edit_text( text="✍️ Введите количество дней, которое хотите добавить к времени действия ключа:", - reply_markup=build_users_key_show_kb(tg_id, email) + reply_markup=build_users_key_show_kb(tg_id, email), ) -@router.callback_query( - AdminUserKeyEditorCallback.filter(F.action == "take"), - IsAdminFilter() -) +@router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "take"), IsAdminFilter()) async def handle_expiry_take( - callback_query: CallbackQuery, - callback_data: AdminUserKeyEditorCallback, - state: FSMContext + callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext ): tg_id = callback_data.tg_id email = callback_data.data @@ -484,18 +368,13 @@ async def handle_expiry_take( await callback_query.message.edit_text( text="✍️ Введите количество дней, которое хотите вычесть из времени действия ключа:", - reply_markup=build_users_key_show_kb(tg_id, email) + reply_markup=build_users_key_show_kb(tg_id, email), ) -@router.callback_query( - AdminUserKeyEditorCallback.filter(F.action == "set"), - IsAdminFilter() -) +@router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "set"), IsAdminFilter()) async def handle_expiry_set( - callback_query: CallbackQuery, - callback_data: AdminUserKeyEditorCallback, - state: FSMContext + callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext ): tg_id = callback_data.tg_id email = callback_data.data @@ -509,21 +388,11 @@ async def handle_expiry_set( "\n Пример: 2025-02-09 09:01" ) - await callback_query.message.edit_text( - text=text, - reply_markup=build_users_key_show_kb(tg_id, email) - ) + await callback_query.message.edit_text(text=text, reply_markup=build_users_key_show_kb(tg_id, email)) -@router.message( - UserEditorState.waiting_for_expiry_time, - IsAdminFilter() -) -async def handle_expiry_time_input( - message: types.Message, - state: FSMContext, - session: Any -): +@router.message(UserEditorState.waiting_for_expiry_time, IsAdminFilter()) +async def handle_expiry_time_input(message: types.Message, state: FSMContext, session: Any): data = await state.get_data() tg_id = data.get("tg_id") email = data.get("email") @@ -532,7 +401,7 @@ async def handle_expiry_time_input( if op_type != "set" and (not message.text.isdigit() or int(message.text) < 0): await message.answer( text="🚫 Пожалуйста, введите корректное количество дней!", - reply_markup=build_users_key_show_kb(tg_id, email) + reply_markup=build_users_key_show_kb(tg_id, email), ) return @@ -555,9 +424,7 @@ async def handle_expiry_time_input( await change_expiry_time(key_details["expiry_time"] - days * 24 * 3600 * 1000, email, session) else: try: - expiry_time = int( - datetime.strptime(message.text, "%Y-%m-%d %H:%M").timestamp() * 1000 - ) + expiry_time = int(datetime.strptime(message.text, "%Y-%m-%d %H:%M").timestamp() * 1000) text = f"✅ Время действия ключа изменено на {message.text}" await change_expiry_time(expiry_time, email, session) except ValueError: @@ -565,21 +432,11 @@ async def handle_expiry_time_input( except Exception as e: text = f"❗ Произошла ошибка во время изменения времени действия ключа: {e}" - await message.answer( - text=text, - reply_markup=build_users_key_show_kb(tg_id, email) - ) + await message.answer(text=text, reply_markup=build_users_key_show_kb(tg_id, email)) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_update_key"), - IsAdminFilter() -) -async def handle_update_key( - callback_query: CallbackQuery, - callback_data: AdminUserEditorCallback, - session: Any -): +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_update_key"), IsAdminFilter()) +async def handle_update_key(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any): tg_id = callback_data.tg_id email = callback_data.data @@ -591,51 +448,32 @@ async def handle_update_key( except Exception as e: logger.error(f"Ошибка при обновлении ключа {email} администратором: {e}") await callback_query.message.answer( - text=f"❗ Произошла ошибка при обновлении ключа: {e}", - reply_markup=build_user_key_kb(tg_id, email) + text=f"❗ Произошла ошибка при обновлении ключа: {e}", reply_markup=build_user_key_kb(tg_id, email) ) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_delete_key"), - IsAdminFilter() -) -async def handle_delete_key( - callback_query: types.CallbackQuery, - callback_data: AdminUserEditorCallback, - session: Any -): +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_key"), IsAdminFilter()) +async def handle_delete_key(callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any): email = callback_data.data - client_id = await session.fetchval( - "SELECT client_id FROM keys WHERE email = $1", email - ) + client_id = await session.fetchval("SELECT client_id FROM keys WHERE email = $1", email) if client_id is None: await callback_query.message.edit_text( - text="🚫 Ключ не найден!", - reply_markup=build_editor_kb(callback_data.tg_id) + text="🚫 Ключ не найден!", reply_markup=build_editor_kb(callback_data.tg_id) ) return await callback_query.message.edit_text( - text="❓ Вы уверены, что хотите удалить ключ?", - reply_markup=build_key_delete_kb(callback_data.tg_id, email) + text="❓ Вы уверены, что хотите удалить ключ?", reply_markup=build_key_delete_kb(callback_data.tg_id, email) ) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_delete_key_confirm"), - IsAdminFilter() -) +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_key_confirm"), IsAdminFilter()) async def handle_delete_key_confirm( - callback_query: types.CallbackQuery, - callback_data: AdminUserEditorCallback, - session: Any + callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any ): email = callback_data.data - record = await session.fetchrow( - "SELECT client_id FROM keys WHERE email = $1", email - ) + record = await session.fetchrow("SELECT client_id FROM keys WHERE email = $1", email) kb = build_editor_kb(callback_data.tg_id) @@ -647,48 +485,28 @@ async def handle_delete_key_confirm( tasks = [] for cluster_name, cluster_servers in clusters.items(): for _ in cluster_servers: - tasks.append( - delete_key_from_cluster(cluster_name, email, client_id) - ) + tasks.append(delete_key_from_cluster(cluster_name, email, client_id)) await asyncio.gather(*tasks) await delete_key_from_servers() await delete_key_from_db(client_id, session) - await callback_query.message.edit_text( - text="✅ Ключ успешно удален.", - reply_markup=kb - ) + await callback_query.message.edit_text(text="✅ Ключ успешно удален.", reply_markup=kb) else: - await callback_query.message.edit_text( - text="🚫 Ключ не найден или уже удален.", - reply_markup=kb - ) + await callback_query.message.edit_text(text="🚫 Ключ не найден или уже удален.", reply_markup=kb) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_delete_user"), - IsAdminFilter() -) -async def handle_delete_user( - callback_query: types.CallbackQuery, - callback_data: AdminUserEditorCallback -): +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_user"), IsAdminFilter()) +async def handle_delete_user(callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback): tg_id = callback_data.tg_id await callback_query.message.edit_text( - text=f"❗️ Вы уверены, что хотите удалить пользователя с ID {tg_id}?", - reply_markup=build_user_delete_kb(tg_id) + text=f"❗️ Вы уверены, что хотите удалить пользователя с ID {tg_id}?", reply_markup=build_user_delete_kb(tg_id) ) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_delete_user_confirm"), - IsAdminFilter() -) +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_user_confirm"), IsAdminFilter()) async def handle_delete_user_confirm( - callback_query: types.CallbackQuery, - callback_data: AdminUserEditorCallback, - session: Any + callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any ): tg_id = callback_data.tg_id key_records = await session.fetch("SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id) @@ -709,8 +527,7 @@ async def handle_delete_user_confirm( try: await delete_user_data(session, tg_id) await callback_query.message.edit_text( - text=f"🗑️ Пользователь с ID {tg_id} был удален.", - reply_markup=build_editor_kb(callback_data.tg_id) + text=f"🗑️ Пользователь с ID {tg_id} был удален.", reply_markup=build_editor_kb(callback_data.tg_id) ) except Exception as e: logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}") @@ -719,37 +536,19 @@ async def handle_delete_user_confirm( ) -@router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_editor"), - IsAdminFilter() -) +@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_editor"), IsAdminFilter()) async def handle_editor( - callback_query: types.CallbackQuery, - callback_data: AdminUserEditorCallback, - state: FSMContext, - session: Any + callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any ): - await process_user_search( - callback_query.message, - state, - session, - callback_data.tg_id, - callback_data.edit - ) + await process_user_search(callback_query.message, state, session, callback_data.tg_id, callback_data.edit) async def process_user_search( - message: types.Message, - state: FSMContext, - session: Any, - tg_id: int, - edit: bool = False + message: types.Message, state: FSMContext, session: Any, tg_id: int, edit: bool = False ) -> None: await state.clear() - balance = await session.fetchval( - "SELECT balance FROM connections WHERE tg_id = $1", tg_id - ) + balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) if balance is None: await message.answer( @@ -758,15 +557,9 @@ async def process_user_search( ) return - username = await session.fetchval( - "SELECT username FROM users WHERE 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 - ) + username = await session.fetchval("SELECT username FROM users WHERE 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) text = ( f"📊 Информация о пользователе" @@ -780,17 +573,11 @@ async def process_user_search( if edit: try: - await message.edit_text( - text=text, - reply_markup=kb - ) + await message.edit_text(text=text, reply_markup=kb) except TelegramBadRequest: pass else: - await message.answer( - text=text, - reply_markup=kb - ) + await message.answer(text=text, reply_markup=kb) async def get_key_details(email, session): @@ -827,9 +614,7 @@ async def change_expiry_time(expiry_time: int, email: str, session: Any) -> Exce if client_id is None: return ValueError(f"User with email {email} was not found") - server_id = await session.fetchrow( - "SELECT server_id FROM keys WHERE client_id = $1", client_id - ) + server_id = await session.fetchrow("SELECT server_id FROM keys WHERE client_id = $1", client_id) if not server_id: return ValueError(f"User with client_id {server_id} was not found") @@ -859,7 +644,8 @@ async def change_expiry_time(expiry_time: int, email: str, session: Any) -> Exce async def get_user_balance(tg_id: int, session: Any) -> float: try: return await session.fetchval( - "SELECT balance FROM connections WHERE tg_id = $1", tg_id, + "SELECT balance FROM connections WHERE tg_id = $1", + tg_id, ) except Exception as e: logger.error(f"Ошибка при получении баланса для пользователя {tg_id}: {e}") @@ -870,7 +656,8 @@ async def add_user_balance(tg_id: int, balance: int, session: Any) -> None: try: await session.execute( "UPDATE connections SET balance = balance + $1 WHERE tg_id = $2", - balance, tg_id, + balance, + tg_id, ) except Exception as e: logger.error(f"Ошибка при добавлении баланса для пользователя {tg_id}: {e}") @@ -880,7 +667,8 @@ async def set_user_balance(tg_id: int, balance: int, session: Any) -> None: try: await session.execute( "UPDATE connections SET balance = $1 WHERE tg_id = $2", - balance, tg_id, + balance, + tg_id, ) except Exception as e: logger.error(f"Ошибка при установке баланса для пользователя {tg_id}: {e}") diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index c1d6b66c..80461348 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -105,7 +105,6 @@ async def create_client_on_server( await asyncio.sleep(0.7) - async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, total_gb): try: servers = await get_servers() diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 787bc25b..7bbf3eb8 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -73,7 +73,7 @@ router = Router() @router.callback_query(F.data == "view_keys") @router.message(F.text == "/subs") async def process_callback_or_message_view_keys( - callback_query_or_message: types.Message | types.CallbackQuery, session: Any + callback_query_or_message: types.Message | types.CallbackQuery, session: Any ): if isinstance(callback_query_or_message, types.CallbackQuery): chat_id = callback_query_or_message.message.chat.id @@ -258,9 +258,7 @@ async def process_callback_update_subscription(callback_query: types.CallbackQue await process_callback_view_key(callback_query, session) except Exception as e: logger.error(f"Ошибка при обновлении ключа {email} пользователем: {e}") - await handle_error( - tg_id, callback_query, f"Ошибка при обновлении подписки: {e}" - ) + await handle_error(tg_id, callback_query, f"Ошибка при обновлении подписки: {e}") @router.callback_query(F.data.startswith("delete_key|")) @@ -335,9 +333,7 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio @router.callback_query(F.data.startswith("confirm_delete|")) -async def process_callback_confirm_delete( - callback_query: types.CallbackQuery, session: Any -): +async def process_callback_confirm_delete(callback_query: types.CallbackQuery, session: Any): email = callback_query.data.split("|")[1] try: record = await get_key_details(email, session) @@ -413,7 +409,8 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery, sessi required_amount = cost - balance logger.info( - f"[RENEW] Пользователю {tg_id} не хватает {required_amount}₽. Запуск доплаты через {USE_NEW_PAYMENT_FLOW}") + f"[RENEW] Пользователю {tg_id} не хватает {required_amount}₽. Запуск доплаты через {USE_NEW_PAYMENT_FLOW}" + ) await create_temporary_data( session, diff --git a/keyboards/admin/bans_kb.py b/keyboards/admin/bans_kb.py index 958d0a14..0c5554d4 100644 --- a/keyboards/admin/bans_kb.py +++ b/keyboards/admin/bans_kb.py @@ -6,16 +6,8 @@ from keyboards.admin.panel_kb import build_admin_back_btn, AdminPanelCallback def build_bans_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button( - text="📄 Выгрузить в CSV", - callback_data=AdminPanelCallback(action="bans_export").pack() - ) - builder.button( - text="🗑️ Удалить из БД", - callback_data=AdminPanelCallback(action="bans_delete_banned").pack() - ) - builder.row( - build_admin_back_btn("management") - ) + builder.button(text="📄 Выгрузить в CSV", callback_data=AdminPanelCallback(action="bans_export").pack()) + builder.button(text="🗑️ Удалить из БД", callback_data=AdminPanelCallback(action="bans_delete_banned").pack()) + builder.row(build_admin_back_btn("management")) builder.adjust(1) return builder.as_markup() diff --git a/keyboards/admin/coupons_kb.py b/keyboards/admin/coupons_kb.py index 41ebd8c3..c24d705c 100644 --- a/keyboards/admin/coupons_kb.py +++ b/keyboards/admin/coupons_kb.py @@ -11,17 +11,9 @@ class AdminCouponDeleteCallback(CallbackData, prefix="admin_coupon_delete"): def build_coupons_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button( - text="➕ Создать купон", - callback_data=AdminPanelCallback(action="coupons_create").pack() - ) - builder.button( - text="Купоны", - callback_data=AdminPanelCallback(action="coupons_list").pack() - ) - builder.row( - build_admin_back_btn() - ) + builder.button(text="➕ Создать купон", callback_data=AdminPanelCallback(action="coupons_create").pack()) + builder.button(text="Купоны", callback_data=AdminPanelCallback(action="coupons_list").pack()) + builder.row(build_admin_back_btn()) return builder.as_markup() @@ -32,12 +24,8 @@ def build_coupons_list_kb(coupons: list) -> InlineKeyboardMarkup: coupon_code = coupon["code"] builder.button( text=f"❌ Удалить {coupon_code}", - callback_data=AdminCouponDeleteCallback( - coupon_code=coupon_code - ).pack(), + callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code).pack(), ) - builder.row( - build_admin_back_btn("coupons") - ) + builder.row(build_admin_back_btn("coupons")) return builder.as_markup() diff --git a/keyboards/admin/panel_kb.py b/keyboards/admin/panel_kb.py index 9b85a216..6b16151f 100644 --- a/keyboards/admin/panel_kb.py +++ b/keyboards/admin/panel_kb.py @@ -9,76 +9,36 @@ class AdminPanelCallback(CallbackData, prefix="admin_panel"): def build_panel_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button( - text="👤 Поиск пользователя", - callback_data=AdminPanelCallback(action="search_user").pack() - ) - builder.button( - text="🔑 Поиск по названию ключа", - callback_data=AdminPanelCallback(action="search_key").pack() - ) + builder.button(text="👤 Поиск пользователя", callback_data=AdminPanelCallback(action="search_user").pack()) + builder.button(text="🔑 Поиск по названию ключа", callback_data=AdminPanelCallback(action="search_key").pack()) builder.row( - InlineKeyboardButton( - text="🖥️ Серверы", - callback_data=AdminPanelCallback(action="servers").pack() - ), - InlineKeyboardButton( - text="🎟️ Купоны", - callback_data=AdminPanelCallback(action="coupons").pack() - ) - ) - builder.button( - text="📢 Рассылка", - callback_data=AdminPanelCallback(action="sender").pack() + InlineKeyboardButton(text="🖥️ Серверы", callback_data=AdminPanelCallback(action="servers").pack()), + InlineKeyboardButton(text="🎟️ Купоны", callback_data=AdminPanelCallback(action="coupons").pack()), ) + builder.button(text="📢 Рассылка", callback_data=AdminPanelCallback(action="sender").pack()) builder.row( - InlineKeyboardButton( - text="📊 Статистика", - callback_data=AdminPanelCallback(action="stats").pack() - ), - InlineKeyboardButton( - text="🤖 Управление", - callback_data=AdminPanelCallback(action="management").pack() - ) - ) - builder.button( - text="Личный кабинет", - callback_data="profile" + InlineKeyboardButton(text="📊 Статистика", callback_data=AdminPanelCallback(action="stats").pack()), + InlineKeyboardButton(text="🤖 Управление", callback_data=AdminPanelCallback(action="management").pack()), ) + builder.button(text="Личный кабинет", callback_data="profile") builder.adjust(1, 1, 2, 1, 2, 1) return builder.as_markup() def build_management_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button( - text="💾 Создать резервную копию", - callback_data=AdminPanelCallback(action="backups").pack() - ) - builder.button( - text="🚫 Заблокировавшие бота", - callback_data=AdminPanelCallback(action="bans").pack() - ) - builder.button( - text="🔄 Перезагрузить бота", - callback_data=AdminPanelCallback(action="restart").pack() - ) - builder.row( - build_admin_back_btn() - ) + builder.button(text="💾 Создать резервную копию", callback_data=AdminPanelCallback(action="backups").pack()) + builder.button(text="🚫 Заблокировавшие бота", callback_data=AdminPanelCallback(action="bans").pack()) + builder.button(text="🔄 Перезагрузить бота", callback_data=AdminPanelCallback(action="restart").pack()) + builder.row(build_admin_back_btn()) builder.adjust(1) return builder.as_markup() def build_restart_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button( - text="✅ Да, перезагрузить", - callback_data=AdminPanelCallback(action="restart_confirm").pack() - ) - builder.row( - build_admin_back_btn() - ) + builder.button(text="✅ Да, перезагрузить", callback_data=AdminPanelCallback(action="restart_confirm").pack()) + builder.row(build_admin_back_btn()) builder.adjust(1) return builder.as_markup() @@ -89,9 +49,7 @@ def build_admin_back_kb(action: str = "admin") -> InlineKeyboardMarkup: def build_admin_singleton_kb(text: str, action: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.row( - build_admin_btn(text, action) - ) + builder.row(build_admin_btn(text, action)) return builder.as_markup() diff --git a/keyboards/admin/sender_kb.py b/keyboards/admin/sender_kb.py index bfe9ad39..d0acfc59 100644 --- a/keyboards/admin/sender_kb.py +++ b/keyboards/admin/sender_kb.py @@ -11,26 +11,9 @@ class AdminSenderCallback(CallbackData, prefix="admin_sender"): def build_sender_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button( - text="👥 Все пользователи", - callback_data=AdminSenderCallback( - type="all" - ).pack() - ) - builder.button( - text="✅ Пользователи с подпиской", - callback_data=AdminSenderCallback( - type="subscribed" - ).pack() - ) - builder.button( - text="❌ Пользователи без подписки", - callback_data=AdminSenderCallback( - type="unsubscribed" - ).pack() - ) - builder.row( - build_admin_back_btn() - ) + builder.button(text="👥 Все пользователи", callback_data=AdminSenderCallback(type="all").pack()) + builder.button(text="✅ Пользователи с подпиской", callback_data=AdminSenderCallback(type="subscribed").pack()) + builder.button(text="❌ Пользователи без подписки", callback_data=AdminSenderCallback(type="unsubscribed").pack()) + builder.row(build_admin_back_btn()) builder.adjust(1) return builder.as_markup() diff --git a/keyboards/admin/servers_kb.py b/keyboards/admin/servers_kb.py index d7dd239c..e857408b 100644 --- a/keyboards/admin/servers_kb.py +++ b/keyboards/admin/servers_kb.py @@ -16,19 +16,11 @@ def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup: for cluster_name in servers: builder.button( text=f"⚙️ {cluster_name}", - callback_data=AdminServerEditorCallback( - action="clusters_manage", - data=cluster_name - ).pack() + callback_data=AdminServerEditorCallback(action="clusters_manage", data=cluster_name).pack(), ) - builder.button( - text="➕ Добавить кластер", - callback_data=AdminPanelCallback(action="clusters_add").pack() - ) - builder.row( - build_admin_back_btn() - ) + builder.button(text="➕ Добавить кластер", callback_data=AdminPanelCallback(action="clusters_add").pack()) + builder.row(build_admin_back_btn()) builder.adjust(1) return builder.as_markup() @@ -39,36 +31,22 @@ def build_manage_cluster_kb(cluster_servers, cluster_name) -> InlineKeyboardMark for server in cluster_servers: builder.button( text=f"🌍 {server['server_name']}", - callback_data=AdminServerEditorCallback( - action="servers_manage", - data=server["server_name"] - ).pack() + callback_data=AdminServerEditorCallback(action="servers_manage", data=server["server_name"]).pack(), ) builder.button( text="➕ Добавить сервер", - callback_data=AdminServerEditorCallback( - action="servers_add", - data=cluster_name - ).pack() + callback_data=AdminServerEditorCallback(action="servers_add", data=cluster_name).pack(), ) builder.button( text="🌐 Доступность серверов", - callback_data=AdminServerEditorCallback( - action="servers_availability", - data=cluster_name - ).pack() + callback_data=AdminServerEditorCallback(action="servers_availability", data=cluster_name).pack(), ) builder.button( text="💾 Создать бэкап кластера", - callback_data=AdminServerEditorCallback( - action="clusters_backup", - data=cluster_name - ).pack() - ) - builder.row( - build_admin_back_btn("servers") + callback_data=AdminServerEditorCallback(action="clusters_backup", data=cluster_name).pack(), ) + builder.row(build_admin_back_btn("servers")) builder.adjust(1) return builder.as_markup() @@ -76,18 +54,10 @@ def build_manage_cluster_kb(cluster_servers, cluster_name) -> InlineKeyboardMark def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( - text="🗑️ Удалить", - callback_data=AdminServerEditorCallback( - action="servers_delete", - data=server_name - ).pack() + text="🗑️ Удалить", callback_data=AdminServerEditorCallback(action="servers_delete", data=server_name).pack() ) builder.button( - text="🔙 Назад", - callback_data=AdminServerEditorCallback( - action="clusters_manage", - data=cluster_name - ).pack() + text="🔙 Назад", callback_data=AdminServerEditorCallback(action="clusters_manage", data=cluster_name).pack() ) builder.adjust(1) return builder.as_markup() @@ -96,18 +66,10 @@ def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboar def build_delete_server_kb(server_name: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( - text="✅ Да", - callback_data=AdminServerEditorCallback( - action="servers_delete_confirm", - data=server_name - ).pack() + text="✅ Да", callback_data=AdminServerEditorCallback(action="servers_delete_confirm", data=server_name).pack() ) builder.button( - text="🔙 Назад", - callback_data=AdminServerEditorCallback( - action="servers_manage", - data=server_name - ).pack() + text="🔙 Назад", callback_data=AdminServerEditorCallback(action="servers_manage", data=server_name).pack() ) builder.adjust(1) return builder.as_markup() diff --git a/keyboards/admin/stats_kb.py b/keyboards/admin/stats_kb.py index 4e1db626..3a6e4092 100644 --- a/keyboards/admin/stats_kb.py +++ b/keyboards/admin/stats_kb.py @@ -6,20 +6,14 @@ from keyboards.admin.panel_kb import build_admin_back_btn, AdminPanelCallback def build_stats_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button( - text="🔄 Обновить", - callback_data=AdminPanelCallback(action="stats").pack() - ) + builder.button(text="🔄 Обновить", callback_data=AdminPanelCallback(action="stats").pack()) builder.button( text="📥 Выгрузить пользователей в CSV", - callback_data=AdminPanelCallback(action="stats_export_users_csv").pack() + callback_data=AdminPanelCallback(action="stats_export_users_csv").pack(), ) builder.button( - text="📥 Выгрузить оплаты в CSV", - callback_data=AdminPanelCallback(action="stats_export_payments_csv").pack() - ) - builder.row( - build_admin_back_btn() + text="📥 Выгрузить оплаты в CSV", callback_data=AdminPanelCallback(action="stats_export_payments_csv").pack() ) + builder.row(build_admin_back_btn()) builder.adjust(1) return builder.as_markup() diff --git a/keyboards/admin/users_kb.py b/keyboards/admin/users_kb.py index 6eb8537c..78378f9d 100644 --- a/keyboards/admin/users_kb.py +++ b/keyboards/admin/users_kb.py @@ -32,47 +32,25 @@ def build_user_edit_kb(tg_id: int, key_records: list) -> InlineKeyboardMarkup: days = (expiry - current_time).days builder.button( text=f"🔑 {email} ({'<1' if days < 1 else days} дн.)", - callback_data=AdminUserEditorCallback( - action="users_key_edit", - tg_id=tg_id, - data=str(email) - ).pack() + callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=str(email)).pack(), ) builder.button( - text="✉️ Сообщение", - callback_data=AdminUserEditorCallback( - action="users_send_message", - tg_id=tg_id - ).pack() + text="✉️ Сообщение", callback_data=AdminUserEditorCallback(action="users_send_message", tg_id=tg_id).pack() ) builder.button( text="💸 Изменить баланс", - callback_data=AdminUserEditorCallback( - action="users_balance_edit", - tg_id=tg_id - ).pack() + callback_data=AdminUserEditorCallback(action="users_balance_edit", tg_id=tg_id).pack(), ) builder.button( text="♻️ Восстановить триал", - callback_data=AdminUserEditorCallback( - action="users_trial_restore", - tg_id=tg_id - ).pack() + callback_data=AdminUserEditorCallback(action="users_trial_restore", tg_id=tg_id).pack(), ) builder.button( - text="❌ Удалить клиента", - callback_data=AdminUserEditorCallback( - action="users_delete_user", - tg_id=tg_id - ).pack() - ) - builder.row( - build_editor_btn("🔄 Обновить данные", tg_id, edit=True) - ) - builder.row( - build_admin_back_btn() + text="❌ Удалить клиента", callback_data=AdminUserEditorCallback(action="users_delete_user", tg_id=tg_id).pack() ) + builder.row(build_editor_btn("🔄 Обновить данные", tg_id, edit=True)) + builder.row(build_admin_back_btn()) builder.adjust(1) return builder.as_markup() @@ -81,10 +59,7 @@ def build_users_balance_change_kb(tg_id: int) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( text="🔙 Назад", # todo: fix magic text was set - callback_data=AdminUserEditorCallback( - action="users_balance_edit", - tg_id=tg_id - ).pack() + callback_data=AdminUserEditorCallback(action="users_balance_edit", tg_id=tg_id).pack(), ) return builder.as_markup() @@ -94,44 +69,23 @@ def build_users_balance_kb(tg_id: int) -> InlineKeyboardMarkup: for month, amount in RENEWAL_PRICES.items(): builder.button( text=f"+ {amount}Р ({month} мес.)", - callback_data=AdminUserEditorCallback( - action="users_balance_add", - tg_id=tg_id, - data=amount - ).pack() + callback_data=AdminUserEditorCallback(action="users_balance_add", tg_id=tg_id, data=amount).pack(), ) builder.button( text=f"- {amount}Р ({month} мес.)", - callback_data=AdminUserEditorCallback( - action="users_balance_add", - tg_id=tg_id, - data=-amount - ).pack() + callback_data=AdminUserEditorCallback(action="users_balance_add", tg_id=tg_id, data=-amount).pack(), ) builder.button( - text="💵 Добавить", - callback_data=AdminUserEditorCallback( - action="users_balance_add", - tg_id=tg_id - ).pack() + text="💵 Добавить", callback_data=AdminUserEditorCallback(action="users_balance_add", tg_id=tg_id).pack() ) builder.button( - text="💵 Вычесть", - callback_data=AdminUserEditorCallback( - action="users_balance_take", - tg_id=tg_id - ).pack() + text="💵 Вычесть", callback_data=AdminUserEditorCallback(action="users_balance_take", tg_id=tg_id).pack() ) builder.button( text="💵 Установить баланс", - callback_data=AdminUserEditorCallback( - action="users_balance_set", - tg_id=tg_id - ).pack() - ) - builder.row( - build_editor_back_btn(tg_id, True) + callback_data=AdminUserEditorCallback(action="users_balance_set", tg_id=tg_id).pack(), ) + builder.row(build_editor_back_btn(tg_id, True)) builder.adjust(2, 2, 2, 2, 2, 1) return builder.as_markup() @@ -140,12 +94,7 @@ def build_users_key_show_kb(tg_id: int, email: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( text="🔙 Назад", # todo: fix magic text was set - callback_data=AdminUserEditorCallback( - action="users_key_edit", - tg_id=tg_id, - data=email, - edit=True - ).pack() + callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email, edit=True).pack(), ) return builder.as_markup() @@ -156,53 +105,25 @@ def build_users_key_expiry_kb(tg_id: int, email: str) -> InlineKeyboardMarkup: month = int(month) builder.button( text=f"+ {month} мес.", - callback_data=AdminUserKeyEditorCallback( - action="add", - tg_id=tg_id, - data=email, - month=month - ).pack() + callback_data=AdminUserKeyEditorCallback(action="add", tg_id=tg_id, data=email, month=month).pack(), ) builder.button( text=f"- {month} мес.", - callback_data=AdminUserKeyEditorCallback( - action="add", - tg_id=tg_id, - data=email, - month=-month - ).pack() + callback_data=AdminUserKeyEditorCallback(action="add", tg_id=tg_id, data=email, month=-month).pack(), ) builder.button( - text="⏳ Добавить дни", - callback_data=AdminUserKeyEditorCallback( - action="add", - tg_id=tg_id, - data=email - ).pack() + text="⏳ Добавить дни", callback_data=AdminUserKeyEditorCallback(action="add", tg_id=tg_id, data=email).pack() ) builder.button( - text="⏳ Вычесть дни", - callback_data=AdminUserKeyEditorCallback( - action="take", - tg_id=tg_id, - data=email - ).pack() + text="⏳ Вычесть дни", callback_data=AdminUserKeyEditorCallback(action="take", tg_id=tg_id, data=email).pack() ) builder.button( text="⏳ Установить дату истечения", - callback_data=AdminUserKeyEditorCallback( - action="set", - tg_id=tg_id, - data=email - ).pack() + callback_data=AdminUserKeyEditorCallback(action="set", tg_id=tg_id, data=email).pack(), ) builder.button( text="🔙 Назад", # todo: fix magic text was set - callback_data=AdminUserEditorCallback( - action="users_key_edit", - tg_id=tg_id, - data=email - ).pack() + callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack(), ) builder.adjust(2, 2, 2, 2, 2, 1) return builder.as_markup() @@ -212,14 +133,9 @@ def build_user_delete_kb(tg_id: int): builder = InlineKeyboardBuilder() builder.button( text="❌ Да, удалить!", - callback_data=AdminUserEditorCallback( - action="users_delete_user_confirm", - tg_id=tg_id - ).pack() - ) - builder.row( - build_editor_back_btn(tg_id, True) + callback_data=AdminUserEditorCallback(action="users_delete_user_confirm", tg_id=tg_id).pack(), ) + builder.row(build_editor_back_btn(tg_id, True)) builder.adjust(1) return builder.as_markup() @@ -227,12 +143,7 @@ def build_user_delete_kb(tg_id: int): def build_user_key_kb(tg_id: int, email: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( - text=f"🔙 Назад", - callback_data=AdminUserEditorCallback( - action="users_key_edit", - tg_id=tg_id, - data=email - ).pack() + text=f"🔙 Назад", callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack() ) builder.adjust(1) return builder.as_markup() @@ -243,30 +154,18 @@ def build_key_edit_kb(key_details: dict, email: str) -> InlineKeyboardMarkup: builder.button( text="⏳ Время истечения", callback_data=AdminUserEditorCallback( - action="users_expiry_edit", - data=email, - tg_id=key_details["tg_id"] - ).pack() + action="users_expiry_edit", data=email, tg_id=key_details["tg_id"] + ).pack(), ) builder.button( text="🔄 Перевыпустить", - callback_data=AdminUserEditorCallback( - action="users_update_key", - data=email, - tg_id=key_details["tg_id"] - ).pack() + callback_data=AdminUserEditorCallback(action="users_update_key", data=email, tg_id=key_details["tg_id"]).pack(), ) builder.button( text="❌ Удалить", - callback_data=AdminUserEditorCallback( - action="users_delete_key", - data=email, - tg_id=key_details["tg_id"] - ).pack() - ) - builder.row( - build_editor_back_btn(key_details["tg_id"], True) + callback_data=AdminUserEditorCallback(action="users_delete_key", data=email, tg_id=key_details["tg_id"]).pack(), ) + builder.row(build_editor_back_btn(key_details["tg_id"], True)) builder.adjust(1) return builder.as_markup() @@ -276,16 +175,10 @@ def build_key_delete_kb(tg_id: int, email: str) -> InlineKeyboardMarkup: builder.row( InlineKeyboardButton( text="✅ Да, удалить", - callback_data=AdminUserEditorCallback( - action="users_delete_key_confirm", - data=email, - tg_id=tg_id - ).pack() + callback_data=AdminUserEditorCallback(action="users_delete_key_confirm", data=email, tg_id=tg_id).pack(), ) ) - builder.row( - build_editor_back_btn(tg_id) - ) + builder.row(build_editor_back_btn(tg_id)) builder.adjust(1) return builder.as_markup() @@ -296,9 +189,7 @@ def build_editor_kb(tg_id: int, edit: bool = False) -> InlineKeyboardMarkup: def build_editor_singleton_kb(text: str, tg_id: int, edit: bool = False) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.row( - build_editor_btn(text, tg_id, edit) - ) + builder.row(build_editor_btn(text, tg_id, edit)) return builder.as_markup() @@ -308,10 +199,5 @@ def build_editor_back_btn(tg_id: int, edit: bool = False) -> InlineKeyboardButto def build_editor_btn(text: str, tg_id: int, edit: bool = False) -> InlineKeyboardButton: return InlineKeyboardButton( - text=text, - callback_data=AdminUserEditorCallback( - action="users_editor", - tg_id=tg_id, - edit=edit - ).pack() + text=text, callback_data=AdminUserEditorCallback(action="users_editor", tg_id=tg_id, edit=edit).pack() ) diff --git a/middlewares/delete.py b/middlewares/delete.py index 29281507..af20634f 100644 --- a/middlewares/delete.py +++ b/middlewares/delete.py @@ -22,20 +22,15 @@ pass_callbacks = [ class DeleteMessageMiddleware(BaseMiddleware): async def __call__( - self, - handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], - event: TelegramObject, - data: dict[str, Any], + self, + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], + event: TelegramObject, + data: dict[str, Any], ) -> Any: if isinstance(event, Message): - if ( - not event.text - or not event.text.startswith("/start") - ): + if not event.text or not event.text.startswith("/start"): try: - await event.bot.delete_message( - event.chat.id, event.message_id - 1 - ) + await event.bot.delete_message(event.chat.id, event.message_id - 1) except Exception: pass await event.delete() diff --git a/utils/csv_export.py b/utils/csv_export.py index 08cc1c59..4b39c023 100644 --- a/utils/csv_export.py +++ b/utils/csv_export.py @@ -35,10 +35,7 @@ async def export_users_csv(session: Any) -> BufferedInputFile: # Перемещение указателя в начало для чтения buffer.seek(0) - return BufferedInputFile( - file=buffer.getvalue().encode("utf-8-sig"), - filename="users_export.csv" - ) + return BufferedInputFile(file=buffer.getvalue().encode("utf-8-sig"), filename="users_export.csv") async def export_payments_csv(session: Any) -> BufferedInputFile: @@ -95,7 +92,4 @@ def _export_payments_csv(payments: list, filename: str) -> BufferedInputFile: # Перемещение указателя в начало для чтения buffer.seek(0) - return BufferedInputFile( - file=buffer.getvalue().encode("utf-8-sig"), - filename=filename - ) + return BufferedInputFile(file=buffer.getvalue().encode("utf-8-sig"), filename=filename) From 8ce7ee0ce045e7d1284d330c0be01441f122db9f Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Wed, 29 Jan 2025 07:15:20 +0300 Subject: [PATCH 40/49] Optimize import statements and remove unused imports across multiple files This commit focuses on cleaning up import statements by: - Removing unused imports - Organizing import order - Removing redundant imports - Ensuring consistent import style across different modules --- backup.py | 7 +- client.py | 7 +- database.py | 1445 +-------------------------- handlers/admin/admin_bans.py | 2 +- handlers/admin/admin_coupons.py | 4 +- handlers/admin/admin_panel.py | 2 +- handlers/admin/admin_restart.py | 2 +- handlers/admin/admin_sender.py | 2 +- handlers/admin/admin_servers.py | 8 +- handlers/admin/admin_user_editor.py | 2 +- handlers/admin/admin_users.py | 20 +- handlers/coupons.py | 1 - handlers/keys/key_management.py | 2 +- handlers/keys/key_utils.py | 2 +- handlers/keys/keys.py | 5 +- handlers/notifications.py | 2 +- keyboards/admin/bans_kb.py | 2 +- keyboards/admin/panel_kb.py | 2 +- keyboards/admin/stats_kb.py | 2 +- keyboards/admin/users_kb.py | 2 +- servers.py | 2 +- 21 files changed, 40 insertions(+), 1483 deletions(-) diff --git a/backup.py b/backup.py index 196e024a..51c6636b 100644 --- a/backup.py +++ b/backup.py @@ -2,7 +2,6 @@ import os import subprocess from datetime import datetime -import aiofiles from aiogram.types import BufferedInputFile from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT @@ -99,9 +98,11 @@ async def create_backup_and_send_to_admins(xui) -> None: async def _send_backup_to_admins(backup_file_path: str) -> None: try: from bot import bot + import aiofiles - with open(backup_file_path, "rb") as backup_file: - backup_input_file = BufferedInputFile(file=backup_file.read(), filename=os.path.basename(backup_file_path)) + async with aiofiles.open(backup_file_path, "rb") as backup_file: + backup_data = await backup_file.read() + backup_input_file = BufferedInputFile(file=backup_data, filename=os.path.basename(backup_file_path)) admin_ids = ADMIN_ID if isinstance(ADMIN_ID, list) else [ADMIN_ID] for admin_id in admin_ids: await bot.send_document(chat_id=admin_id, document=backup_input_file) diff --git a/client.py b/client.py index 8c4d452c..984a662a 100644 --- a/client.py +++ b/client.py @@ -1,5 +1,6 @@ from dataclasses import dataclass -from typing import Optional, Dict, Any, List +from typing import Any + import py3xui from config import LIMIT_IP, SUPERNODE @@ -22,7 +23,7 @@ class ClientConfig: sub_id: str -async def add_client(xui: py3xui.API, config: ClientConfig) -> Dict[str, Any]: +async def add_client(xui: py3xui.API, config: ClientConfig) -> dict[str, Any]: """ Добавляет клиента на сервер через 3x-ui. @@ -66,7 +67,7 @@ async def add_client(xui: py3xui.API, config: ClientConfig) -> Dict[str, Any]: async def extend_client_key( xui: py3xui.API, inbound_id: int, email: str, new_expiry_time: int, client_id: str, total_gb: int, sub_id: str -) -> Optional[bool]: +) -> bool | None: """ Обновляет срок действия ключа клиента. diff --git a/database.py b/database.py index b526d510..2492ddc6 100644 --- a/database.py +++ b/database.py @@ -1567,1447 +1567,4 @@ async def get_all_keys(session: Any = None): raise finally: if conn is not None and session is None: - await conn.close() - - -import json -from datetime import datetime -from typing import Any - -import asyncpg -import pytz - -from config import DATABASE_URL, REFERRAL_BONUS_PERCENTAGES -from logger import logger - - -async def create_temporary_data(session, tg_id: int, state: str, data: dict): - """Сохраняет временные данные пользователя.""" - await session.execute( - """ - INSERT INTO temporary_data (tg_id, state, data, updated_at) - VALUES ($1, $2, $3, $4) - ON CONFLICT (tg_id) - DO UPDATE SET state = $2, data = $3, updated_at = $4 - """, - tg_id, - state, - json.dumps(data), - datetime.utcnow(), - ) - - -async def get_temporary_data(session, tg_id: int) -> dict | None: - """Извлекает временные данные пользователя.""" - result = await session.fetchrow("SELECT state, data FROM temporary_data WHERE tg_id = $1", tg_id) - if result: - return {"state": result["state"], "data": json.loads(result["data"])} - return None - - -async def clear_temporary_data(session, tg_id: int): - # TODO rename delete_temporary_data - await session.execute("DELETE FROM temporary_data WHERE tg_id = $1", tg_id) - - -async def create_blocked_user(tg_id: int, conn: asyncpg.Connection): - await conn.execute( - "INSERT INTO blocked_users (tg_id) VALUES ($1) ON CONFLICT (tg_id) DO NOTHING", - tg_id, - ) - - -async def delete_blocked_user(tg_id: int | list[int], conn: asyncpg.Connection): - """ - Удаляет пользователя или список пользователей из списка заблокированных. - - :param tg_id: ID пользователя Telegram или список ID - :param conn: Подключение к базе данных - """ - if isinstance(tg_id, list): - await conn.execute("DELETE FROM blocked_users WHERE tg_id = ANY($1)", tg_id) - else: - await conn.execute("DELETE FROM blocked_users WHERE tg_id = $1", tg_id) - - -async def init_db(file_path: str = "assets/schema.sql"): - with open(file_path) as file: - sql_content = file.read() - - statements = [stmt.strip() for stmt in sql_content.split(";") if stmt.strip()] - conn = await asyncpg.connect(DATABASE_URL) - - try: - for statement in statements: - await conn.execute(statement) - except Exception as e: - logger.error(f"Error while executing SQL statement: {e}") - finally: - logger.info("Tables created successfully") - await conn.close() - - -async def check_unique_server_name(server_name: str, session: Any, cluster_name: str | None = None) -> bool: - """ - Проверяет уникальность имени сервера. - - :param server_name: Имя сервера. - :param session: Сессия базы данных. - :param cluster_name: Имя кластера (опционально). - :return: True, если имя сервера уникально, False, если уже существует. - """ - if cluster_name: - result = await session.fetchrow( - "SELECT 1 FROM servers WHERE server_name = $1 AND cluster_name = $2 LIMIT 1", server_name, cluster_name - ) - else: - result = await session.fetchrow("SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", server_name) - - return result is None - - -async def create_coupon(coupon_code: str, amount: float, usage_limit: int, session: Any): - """ - Создает новый купон в базе данных. - - Args: - coupon_code (str): Уникальный код купона. - amount (float): Сумма, которую дает купон. - usage_limit (int): Максимальное количество использований купона. - session (Any): Сессия базы данных для выполнения запроса. - - Raises: - Exception: В случае ошибки при создании купона. - - Example: - await create_coupon('SALE50', 50.0, 5, session) - """ - try: - await session.execute( - """ - INSERT INTO coupons (code, amount, usage_limit, usage_count, is_used) - VALUES ($1, $2, $3, 0, FALSE) - """, - coupon_code, - amount, - usage_limit, - ) - logger.info(f"Успешно создан купон с кодом {coupon_code} на сумму {amount}") - except Exception as e: - logger.error(f"Ошибка при создании купона {coupon_code}: {e}") - raise - - -async def get_coupon_by_code(coupon_code: str, session: Any) -> dict | None: - """ - Получает информацию о купоне по его коду. - - Args: - coupon_code (str): Код купона для поиска - session (Any): Сессия базы данных - - Returns: - dict | None: Словарь с информацией о купоне или None, если купон не найден - - id (int): ID купона - - usage_limit (int): Лимит использований - - usage_count (int): Текущее количество использований - - is_used (bool): Флаг использования - - amount (float): Сумма купона - - Raises: - Exception: В случае ошибки при выполнении запроса - """ - try: - result = await session.fetchrow( - """ - SELECT id, usage_limit, usage_count, is_used, amount - FROM coupons - WHERE code = $1 AND (usage_count < usage_limit OR usage_limit = 0) AND is_used = FALSE - """, - coupon_code, - ) - return dict(result) if result else None - except Exception as e: - logger.error(f"Ошибка при получении купона {coupon_code}: {e}") - raise - - -async def get_all_coupons(session: Any, page: int = 1, per_page: int = 10): - """ - Получает список купонов из базы данных с пагинацией. - - Args: - session (Any): Сессия базы данных для выполнения запроса - page (int): Номер страницы (по умолчанию 1) - per_page (int): Количество купонов на странице (по умолчанию 10) - - Returns: - dict: Словарь с информацией о купонах и пагинации: - - coupons (list): Список словарей с информацией о купонах - - total (int): Общее количество купонов - - pages (int): Общее количество страниц - - current_page (int): Текущая страница - - Raises: - Exception: В случае ошибки при получении данных из базы - """ - try: - offset = (page - 1) * per_page - coupons = await session.fetch( - """ - SELECT code, amount, usage_limit, usage_count - FROM coupons - ORDER BY id - LIMIT $1 OFFSET $2 - """, - per_page, - offset, - ) - - total_count = await session.fetchval("SELECT COUNT(*) FROM coupons") - total_pages = -(-total_count // per_page) # Округление вверх - - logger.info(f"Успешно получено {len(coupons)} купонов из базы данных (страница {page})") - - return {"coupons": coupons, "total": total_count, "pages": total_pages, "current_page": page} - except Exception as e: - logger.error(f"Критическая ошибка при получении списка купонов: {e}") - logger.exception("Трассировка стека ошибки получения купонов") - return {"coupons": [], "total": 0, "pages": 0, "current_page": page} - - -async def delete_coupon(coupon_code: str, session: Any): - """ - Удаляет купон из базы данных по его коду. - - Args: - coupon_code (str): Код купона для удаления - session (Any): Сессия базы данных для выполнения запроса - - Returns: - bool: True, если купон успешно удален, False если купон не найден или произошла ошибка - - Raises: - Exception: В случае ошибки при выполнении запроса к базе данных - - Example: - result = await delete_coupon('SALE50', session) - """ - try: - coupon_record = await session.fetchrow( - """ - SELECT id FROM coupons WHERE code = $1 - """, - coupon_code, - ) - - if not coupon_record: - logger.info(f"Купон {coupon_code} не найден в базе данных") - return False - - await session.execute( - """ - DELETE FROM coupons WHERE code = $1 - """, - coupon_code, - ) - - logger.info(f"Купон {coupon_code} успешно удален из базы данных") - return True - - except Exception as e: - logger.error(f"Произошла ошибка при удалении купона {coupon_code}: {e}") - return False - - -async def set_trial(tg_id: int, status: int, session: Any): - """ - Устанавливает статус триального периода для пользователя. - - Args: - tg_id (int): Telegram ID пользователя - status (int): Статус триального периода (0 - доступен, 1 - использован) - session (Any): Сессия базы данных - - Returns: - bool: True, если статус успешно установлен, False в случае ошибки - """ - try: - await session.execute( - """ - INSERT INTO connections (tg_id, trial) - VALUES ($1, $2) - ON CONFLICT (tg_id) - DO UPDATE SET trial = $2 - """, - tg_id, - status, - ) - status_text = "восстановлен" if status == 0 else "использован" - logger.info(f"Триальный период успешно {status_text} для пользователя {tg_id}") - return True - except Exception as e: - logger.error(f"Ошибка при установке статуса триального периода для пользователя {tg_id}: {e}") - return False - - -async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0, session: Any = None): - """ - Добавляет новое подключение для пользователя в базу данных. - - Args: - tg_id (int): Telegram ID пользователя - balance (float, optional): Начальный баланс пользователя. По умолчанию 0.0. - trial (int, optional): Статус триального периода. По умолчанию 0. - session (Any, optional): Сессия базы данных. - - Raises: - Exception: Если возникает ошибка при добавлении подключения в базу данных. - """ - try: - await session.execute( - """ - INSERT INTO connections (tg_id, balance, trial) - VALUES ($1, $2, $3) - """, - tg_id, - balance, - trial, - ) - logger.info( - f"Успешно добавлено новое подключение для пользователя {tg_id} с балансом {balance} и статусом триала {trial}" - ) - except Exception as e: - logger.error(f"Не удалось добавить подключение для пользователя {tg_id}. Причина: {e}") - raise - - -async def check_connection_exists(tg_id: int): - """ - Проверяет существование подключения для указанного пользователя в базе данных. - - Args: - tg_id (int): Telegram ID пользователя для проверки. - - Returns: - bool: True, если подключение существует, иначе False. - - Raises: - Exception: В случае ошибки при подключении к базе данных. - """ - try: - conn = await asyncpg.connect(DATABASE_URL) - exists = await conn.fetchval( - """ - SELECT EXISTS(SELECT 1 FROM connections WHERE tg_id = $1) - """, - tg_id, - ) - logger.info( - f"Проверка существования подключения для пользователя {tg_id}: {'найдено' if exists else 'не найдено'}" - ) - return exists - except Exception as e: - logger.error(f"Ошибка при проверке подключения для пользователя {tg_id}: {e}") - raise - finally: - if conn: - await conn.close() - - -async def store_key( - tg_id: int, - client_id: str, - email: str, - expiry_time: int, - key: str, - server_id: str, - session: Any, -): - """ - Сохраняет информацию о ключе в базу данных. - - Args: - tg_id (int): Telegram ID пользователя - client_id (str): Уникальный идентификатор клиента - email (str): Электронная почта или имя устройства - expiry_time (int): Время истечения ключа в миллисекундах - key (str): Ключ доступа - server_id (str): Идентификатор сервера - - Raises: - Exception: Если возникает ошибка при сохранении ключа в базу данных - """ - try: - await session.execute( - """ - INSERT INTO keys (tg_id, client_id, email, created_at, expiry_time, key, server_id) - VALUES ($1, $2, $3, $4, $5, $6, $7) - """, - tg_id, - client_id, - email, - int(datetime.utcnow().timestamp() * 1000), - expiry_time, - key, - server_id, - ) - logger.info(f"Ключ успешно сохранен для пользователя {tg_id} на сервере {server_id}") - except Exception as e: - logger.error(f"Ошибка при сохранении ключа для пользователя {tg_id}: {e}") - raise - - -async def get_keys(tg_id: int, session: Any): - """ - Получает список ключей для указанного пользователя. - - Args: - tg_id (int): Telegram ID пользователя - - Returns: - list: Список записей ключей с информацией о клиенте, электронной почте, времени создания и ключе - - Raises: - Exception: В случае ошибки при подключении к базе данных или выполнении запроса - """ - try: - records = await session.fetch( - """ - SELECT client_id, email, created_at, key - FROM keys - WHERE tg_id = $1 - """, - tg_id, - ) - logger.info(f"Успешно получено {len(records)} ключей для пользователя {tg_id}") - return records - except Exception as e: - logger.error(f"Ошибка при получении ключей для пользователя {tg_id}: {e}") - raise - - -async def get_keys_by_server(tg_id: int | None, server_id: str, session: Any): - """ - Получает список ключей на определенном сервере. Если tg_id=None, возвращает все ключи на сервере. - - Args: - tg_id (int | None): Telegram ID пользователя или None для всех пользователей - server_id (str): Идентификатор сервера - - Returns: - list: Список записей ключей с информацией о клиенте, электронной почте, времени создания и ключе - - Raises: - Exception: В случае ошибки при подключении к базе данных или выполнении запроса - """ - try: - if tg_id is not None: - records = await session.fetch( - """ - SELECT * - FROM keys - WHERE tg_id = $1 AND server_id = $2 - """, - tg_id, - server_id, - ) - logger.info(f"Успешно получено {len(records)} ключей для пользователя {tg_id} на сервере {server_id}") - else: - records = await session.fetch( - """ - SELECT * - FROM keys - WHERE server_id = $1 - """, - server_id, - ) - logger.info(f"Успешно получено {len(records)} ключей на сервере {server_id}") - - return records - except Exception as e: - error_msg = f"Ошибка при получении ключей на сервере {server_id}" - if tg_id is not None: - error_msg += f" для пользователя {tg_id}" - logger.error(f"{error_msg}: {e}") - raise - - -async def get_balance(tg_id: int) -> float: - """ - Получает баланс пользователя из базы данных. - - Args: - tg_id (int): Telegram ID пользователя - - Returns: - float: Баланс пользователя, 0.0 если баланс не найден - - Raises: - Exception: В случае ошибки при подключении к базе данных или выполнении запроса - """ - conn = None - try: - conn = await asyncpg.connect(DATABASE_URL) - balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) - logger.info(f"Получен баланс для пользователя {tg_id}: {balance}") - return round(balance, 1) if balance is not None else 0.0 - except Exception as e: - logger.error(f"Ошибка при получении баланса для пользователя {tg_id}: {e}") - return 0.0 - finally: - if conn: - await conn.close() - - -async def update_balance(tg_id: int, amount: float, session: Any = None): - """ - Обновляет баланс пользователя в базе данных. - - Args: - tg_id (int): Telegram ID пользователя - amount (float): Сумма для обновления баланса - session (Any, optional): Сессия базы данных. Если не передана, создается новая. - - Raises: - Exception: В случае ошибки при подключении к базе данных или обновлении баланса - """ - conn = None - try: - if session is None: - conn = await asyncpg.connect(DATABASE_URL) - session = conn - - await session.execute( - """ - UPDATE connections - SET balance = balance + $1 - WHERE tg_id = $2 - """, - amount, - tg_id, - ) - logger.info(f"Баланс пользователя {tg_id} обновлен на сумму {amount}") - - await handle_referral_on_balance_update(tg_id, amount) - - except Exception as e: - logger.error(f"Ошибка при обновлении баланса для пользователя {tg_id}: {e}") - raise - finally: - if conn is not None: - await conn.close() - - -async def get_trial(tg_id: int, session: Any) -> int: - """ - Получает статус триала для пользователя из базы данных. - - Args: - tg_id (int): Telegram ID пользователя - session (Any): Сессия базы данных - - Returns: - int: Статус триала (0 - не использован, 1 - использован) - """ - try: - trial = await session.fetchval("SELECT trial FROM connections WHERE tg_id = $1", tg_id) - logger.info(f"Получен статус триала для пользователя {tg_id}: {trial}") - return trial if trial is not None else 0 - except Exception as e: - logger.error(f"Ошибка при получении статуса триала для пользователя {tg_id}: {e}") - return 0 - - -async def get_key_count(tg_id: int) -> int: - """ - Получает количество ключей для указанного пользователя. - - Args: - tg_id (int): Telegram ID пользователя - - Returns: - int: Количество ключей пользователя, 0 если ключей нет - - Raises: - Exception: В случае ошибки при подключении к базе данных - """ - conn = None - try: - conn = await asyncpg.connect(DATABASE_URL) - count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE tg_id = $1", tg_id) - logger.info(f"Получено количество ключей для пользователя {tg_id}: {count}") - return count if count is not None else 0 - except Exception as e: - logger.error(f"Ошибка при получении количества ключей для пользователя {tg_id}: {e}") - return 0 - finally: - if conn: - await conn.close() - - -async def add_referral(referred_tg_id: int, referrer_tg_id: int, session: Any): - try: - if referred_tg_id == referrer_tg_id: - logger.warning(f"Пользователь {referred_tg_id} попытался использовать свою собственную реферальную ссылку.") - return - - await session.execute( - """ - INSERT INTO referrals (referred_tg_id, referrer_tg_id) - VALUES ($1, $2) - """, - referred_tg_id, - referrer_tg_id, - ) - logger.info(f"Добавлена реферальная связь: приглашенный {referred_tg_id}, пригласивший {referrer_tg_id}") - except Exception as e: - logger.error(f"Ошибка при добавлении реферала: {e}") - raise - - -async def handle_referral_on_balance_update(tg_id: int, amount: float): - """ - Обработка многоуровневой реферальной системы при обновлении баланса пользователя. - - Метод анализирует цепочку рефералов для указанного пользователя и начисляет - бонусы реферерам на разных уровнях согласно настроенным процентам. - - Args: - tg_id (int): Идентификатор Telegram пользователя, пополнившего баланс - amount (float): Сумма пополнения баланса - """ - conn = None - try: - conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Начало обработки реферальной системы для пользователя {tg_id}") - - MAX_REFERRAL_LEVELS = len(REFERRAL_BONUS_PERCENTAGES.keys()) - if MAX_REFERRAL_LEVELS == 0: - logger.warning("Реферальные бонусы отключены.") - return - - visited_tg_ids = set() - current_tg_id = tg_id - referral_chain = [] - - for level in range(1, MAX_REFERRAL_LEVELS + 1): - if current_tg_id in visited_tg_ids: - logger.warning(f"Обнаружен цикл в реферальной цепочке для пользователя {current_tg_id}. Прекращение.") - break - - visited_tg_ids.add(current_tg_id) - - referral = await conn.fetchrow( - """ - SELECT referrer_tg_id - FROM referrals - WHERE referred_tg_id = $1 - """, - current_tg_id, - ) - - if not referral: - logger.info(f"Цепочка рефералов завершена на уровне {level}.") - break - - referrer_tg_id = referral["referrer_tg_id"] - - if referrer_tg_id in visited_tg_ids: - logger.warning(f"Реферер {referrer_tg_id} уже обработан. Пропуск.") - break - - referral_chain.append({"tg_id": referrer_tg_id, "level": level}) - current_tg_id = referrer_tg_id - - for referral in referral_chain: - referrer_tg_id = referral["tg_id"] - level = referral["level"] - - bonus_percent = REFERRAL_BONUS_PERCENTAGES.get(level, 0) - if bonus_percent <= 0: - logger.warning(f"Процент бонуса для уровня {level} равен 0. Пропуск.") - continue - - bonus = round(amount * bonus_percent, 2) - - if bonus > 0: - logger.info(f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id} на уровне {level}.") - await update_balance(referrer_tg_id, bonus) - - except Exception as e: - logger.error(f"Ошибка при обработке многоуровневой реферальной системы для {tg_id}: {e}") - finally: - if conn: - await conn.close() - - -async def get_referral_stats(referrer_tg_id: int): - conn = None - try: - conn = await asyncpg.connect(DATABASE_URL) - logger.info( - f"Установлено подключение к базе данных для получения статистики рефералов пользователя {referrer_tg_id}" - ) - - total_referrals = await conn.fetchval( - """ - SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1 - """, - referrer_tg_id, - ) - logger.debug(f"Получено общее количество рефералов: {total_referrals}") - - active_referrals = await conn.fetchval( - """ - SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1 AND reward_issued = TRUE - """, - referrer_tg_id, - ) - logger.debug(f"Получено количество активных рефералов: {active_referrals}") - - MAX_REFERRAL_LEVELS = len(REFERRAL_BONUS_PERCENTAGES.keys()) - - referrals_by_level_records = await conn.fetch( - f""" - WITH RECURSIVE referral_levels AS ( - SELECT referred_tg_id, referrer_tg_id, 1 AS level - FROM referrals - WHERE referrer_tg_id = $1 - - UNION - - SELECT r.referred_tg_id, r.referrer_tg_id, rl.level + 1 - FROM referrals r - JOIN referral_levels rl ON r.referrer_tg_id = rl.referred_tg_id - WHERE rl.level < {MAX_REFERRAL_LEVELS} - ) - SELECT level, - COUNT(*) AS level_count, - COUNT(CASE WHEN reward_issued = TRUE THEN 1 END) AS active_level_count - FROM referral_levels rl - JOIN referrals r ON rl.referred_tg_id = r.referred_tg_id - GROUP BY level - ORDER BY level - """, - referrer_tg_id, - ) - - referrals_by_level = { - record["level"]: { - "total": record["level_count"], - "active": record["active_level_count"], - } - for record in referrals_by_level_records - } - logger.debug(f"Получена статистика рефералов по уровням: {referrals_by_level}") - - total_referral_bonus = await conn.fetchval( - f""" - WITH RECURSIVE referral_levels AS ( - SELECT - referred_tg_id, - referrer_tg_id, - 1 AS level - FROM referrals - WHERE referrer_tg_id = $1 - - UNION - - SELECT - r.referred_tg_id, - r.referrer_tg_id, - rl.level + 1 - FROM referrals r - JOIN referral_levels rl ON r.referrer_tg_id = rl.referred_tg_id - WHERE rl.level < {MAX_REFERRAL_LEVELS} - ) - SELECT - COALESCE(SUM(p.amount * ( - CASE - {" ".join([f"WHEN rl.level = {level} THEN {REFERRAL_BONUS_PERCENTAGES[level]}" for level in REFERRAL_BONUS_PERCENTAGES])} - ELSE 0 - END)), 0) AS total_bonus - FROM referral_levels rl - JOIN payments p ON rl.referred_tg_id = p.tg_id - WHERE p.status = 'success' AND rl.level <= {MAX_REFERRAL_LEVELS} - """, - referrer_tg_id, - ) - - logger.debug(f"Получена общая сумма бонусов от рефералов: {total_referral_bonus}") - - return { - "total_referrals": total_referrals, - "active_referrals": active_referrals, - "referrals_by_level": referrals_by_level, - "total_referral_bonus": total_referral_bonus, - } - - except Exception as e: - logger.error(f"Ошибка при получении статистики рефералов для пользователя {referrer_tg_id}: {e}") - raise - finally: - if conn: - await conn.close() - logger.info("Закрытие подключения к базе данных") - - -async def update_key_expiry(client_id: str, new_expiry_time: int, session: Any): - """ - Обновление времени истечения ключа для указанного клиента. - - Args: - client_id (str): Уникальный идентификатор клиента - new_expiry_time (int): Новое время истечения ключа - - Raises: - Exception: В случае ошибки при подключении к базе данных или обновлении ключа - """ - try: - await session.execute( - """ - UPDATE keys - SET expiry_time = $1, notified = FALSE, notified_24h = FALSE - WHERE client_id = $2 - """, - new_expiry_time, - client_id, - ) - logger.info(f"Успешно обновлено время истечения ключа для клиента {client_id}") - - except Exception as e: - logger.error(f"Ошибка при обновлении времени истечения ключа для клиента {client_id}: {e}") - raise - - -async def add_balance_to_client(client_id: str, amount: float): - """ - Добавление баланса клиенту по его идентификатору Telegram. - - Args: - client_id (str): Идентификатор клиента в Telegram - amount (float): Сумма для пополнения баланса - - Raises: - Exception: В случае ошибки при подключении к базе данных или обновлении баланса - """ - conn = None - try: - conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для пополнения баланса клиента {client_id}") - - await conn.execute( - """ - UPDATE connections - SET balance = balance + $1 - WHERE tg_id = $2 - """, - amount, - client_id, - ) - logger.info(f"Успешно пополнен баланс клиента {client_id} на сумму {amount}") - - except Exception as e: - logger.error(f"Ошибка при пополнении баланса для клиента {client_id}: {e}") - raise - finally: - if conn: - await conn.close() - logger.info("Закрытие подключения к базе данных") - - -async def get_client_id_by_email(email: str): - """ - Получение идентификатора клиента по электронной почте. - - Args: - email (str): Электронная почта клиента - - Returns: - str: Идентификатор клиента или None, если клиент не найден - - Raises: - Exception: В случае ошибки при подключении к базе данных или выполнении запроса - """ - conn = None - try: - conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для поиска client_id по email: {email}") - - client_id = await conn.fetchval( - """ - SELECT client_id FROM keys WHERE email = $1 - """, - email, - ) - - if client_id: - logger.info(f"Найден client_id для email: {email}") - else: - logger.warning(f"Не найден client_id для email: {email}") - - return client_id - - except Exception as e: - logger.error(f"Ошибка при получении client_id для email {email}: {e}") - raise - finally: - if conn: - await conn.close() - logger.info("Закрытие подключения к базе данных") - - -async def get_tg_id_by_client_id(client_id: str): - """ - Получение Telegram ID по идентификатору клиента. - - Args: - client_id (str): Идентификатор клиента - - Returns: - int или None: Telegram ID клиента, если найден, иначе None - - Raises: - Exception: В случае ошибки при подключении к базе данных или выполнении запроса - """ - conn = None - try: - conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для поиска Telegram ID по client_id: {client_id}") - - result = await conn.fetchrow("SELECT tg_id FROM keys WHERE client_id = $1", client_id) - - if result: - logger.info(f"Найден Telegram ID для client_id: {client_id}") - return result["tg_id"] - else: - logger.warning(f"Не найден Telegram ID для client_id: {client_id}") - return None - - except Exception as e: - logger.error(f"Ошибка при получении Telegram ID для client_id {client_id}: {e}") - raise - finally: - if conn: - await conn.close() - logger.info("Закрытие подключения к базе данных") - - -async def upsert_user( - tg_id: int, - username: str = None, - first_name: str = None, - last_name: str = None, - language_code: str = None, - is_bot: bool = False, -): - """ - Обновляет или вставляет информацию о пользователе в базу данных. - - Args: - tg_id (int): Идентификатор пользователя в Telegram - username (str, optional): Имя пользователя в Telegram - first_name (str, optional): Имя пользователя - last_name (str, optional): Фамилия пользователя - language_code (str, optional): Код языка пользователя - is_bot (bool, optional): Флаг, указывающий является ли пользователь ботом - - Raises: - Exception: В случае ошибки при работе с базой данных - """ - conn = None - try: - conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для обновления пользователя {tg_id}") - - await conn.execute( - """ - INSERT INTO users (tg_id, username, first_name, last_name, language_code, is_bot, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) - ON CONFLICT (tg_id) DO UPDATE - SET - username = COALESCE(EXCLUDED.username, users.username), - first_name = COALESCE(EXCLUDED.first_name, users.first_name), - last_name = COALESCE(EXCLUDED.last_name, users.last_name), - language_code = COALESCE(EXCLUDED.language_code, users.language_code), - is_bot = EXCLUDED.is_bot, - updated_at = CURRENT_TIMESTAMP - """, - tg_id, - username, - first_name, - last_name, - language_code, - is_bot, - ) - logger.info(f"Успешно обновлена информация о пользователе {tg_id}") - except Exception as e: - logger.error(f"Ошибка при обновлении информации о пользователе {tg_id}: {e}") - raise - finally: - if conn: - await conn.close() - logger.info("Закрытие подключения к базе данных") - - -async def add_payment(tg_id: int, amount: float, payment_system: str): - """ - Добавляет информацию о платеже в базу данных. - - Args: - tg_id (int): Идентификатор пользователя в Telegram - amount (float): Сумма платежа - payment_system (str): Система оплаты - - Raises: - Exception: В случае ошибки при добавлении платежа - """ - conn = None - try: - conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для добавления платежа пользователя {tg_id}") - - await conn.execute( - """ - INSERT INTO payments (tg_id, amount, payment_system, status) - VALUES ($1, $2, $3, 'success') - """, - tg_id, - amount, - payment_system, - ) - logger.info(f"Успешно добавлен платеж для пользователя {tg_id} на сумму {amount}") - except Exception as e: - logger.error(f"Ошибка при добавлении платежа для пользователя {tg_id}: {e}") - raise - finally: - if conn: - await conn.close() - logger.info("Закрытие подключения к базе данных после добавления платежа") - - -async def add_notification(tg_id: int, notification_type: str, session: Any): - """ - Добавляет запись о notification в базу данных. - - Args: - tg_id (int): Идентификатор пользователя в Telegram - notification_type (str): Тип уведомления - session (Any): Сессия базы данных для выполнения запроса - - Raises: - Exception: В случае ошибки при добавлении notification - """ - try: - await session.execute( - """ - INSERT INTO notifications (tg_id, notification_type) - VALUES ($1, $2) - ON CONFLICT (tg_id, notification_type) - DO UPDATE SET last_notification_time = NOW() - """, - tg_id, - notification_type, - ) - logger.info(f"Успешно добавлено уведомление типа {notification_type} для пользователя {tg_id}") - except Exception as e: - logger.error(f"Ошибка при добавлении notification для пользователя {tg_id}: {e}") - raise - - -async def check_notification_time(tg_id: int, notification_type: str, hours: int = 12, session: Any = None) -> bool: - """ - Проверяет, прошло ли указанное количество часов с момента последнего уведомления. - - Args: - tg_id (int): Идентификатор пользователя в Telegram - notification_type (str): Тип уведомления - hours (int, optional): Количество часов для проверки. По умолчанию 12. - session (Any): Сессия базы данных для выполнения запроса - - Returns: - bool: True, если с момента последнего уведомления прошло больше указанного времени, иначе False - - Raises: - Exception: В случае ошибки при проверке времени уведомления - """ - conn = None - try: - conn = session if session is not None else await asyncpg.connect(DATABASE_URL) - - result = await conn.fetchval( - """ - SELECT - CASE - WHEN MAX(last_notification_time) IS NULL THEN TRUE - WHEN NOW() - MAX(last_notification_time) > ($1 * INTERVAL '1 hour') THEN TRUE - ELSE FALSE - END AS can_notify - FROM notifications - WHERE tg_id = $2 AND notification_type = $3 - """, - hours, - tg_id, - notification_type, - ) - - can_notify = result if result is not None else True - - logger.info( - f"Проверка уведомления типа {notification_type} для пользователя {tg_id}: {'можно отправить' if can_notify else 'слишком рано'}" - ) - - return can_notify - - except Exception as e: - logger.error(f"Ошибка при проверке времени уведомления для пользователя {tg_id}: {e}") - return False - - finally: - if conn is not None and session is None: - await conn.close() - - -async def get_servers(session: Any = None): - conn = None - try: - conn = session if session is not None else await asyncpg.connect(DATABASE_URL) - - result = await conn.fetch( - """ - SELECT cluster_name, server_name, api_url, subscription_url, inbound_id - FROM servers - """ - ) - servers = {} - for row in result: - cluster_name = row["cluster_name"] - if cluster_name not in servers: - servers[cluster_name] = [] - - servers[cluster_name].append( - { - "server_name": row["server_name"], - "api_url": row["api_url"], - "subscription_url": row["subscription_url"], - "inbound_id": row["inbound_id"], - } - ) - - return servers - - finally: - if conn is not None and session is None: - await conn.close() - - -async def delete_user_data(session: Any, tg_id: int): - try: - await session.execute("DELETE FROM gifts WHERE sender_tg_id = $1 OR recipient_tg_id = $1", tg_id) - except Exception as e: - logger.warning(f"У Вас версия без подарков для {tg_id}: {e}") - await session.execute("DELETE FROM payments WHERE tg_id = $1", tg_id) - await session.execute("DELETE FROM users WHERE tg_id = $1", tg_id) - await session.execute("DELETE FROM connections WHERE tg_id = $1", tg_id) - await delete_key(tg_id, session) - await session.execute("DELETE FROM referrals WHERE referrer_tg_id = $1", tg_id) - - -async def store_gift_link( - gift_id: str, - sender_tg_id: int, - selected_months: int, - expiry_time: datetime, - gift_link: str, - session: Any = None, -): - """ - Добавляет информацию о подарке в базу данных. - - Args: - gift_id (str): Уникальный идентификатор подарка - sender_tg_id (int): Идентификатор пользователя, который отправил подарок - selected_months (int): Количество месяцев подписки - expiry_time (datetime): Время окончания подписки - gift_link (str): Ссылка для активации подарка - session (Any): Сессия базы данных для выполнения запроса - - Returns: - bool: True, если информация о подарке успешно добавлена, иначе False - - Raises: - Exception: В случае ошибки при сохранении информации о подарке - """ - conn = None - try: - conn = session if session is not None else await asyncpg.connect(DATABASE_URL) - - result = await conn.execute( - """ - INSERT INTO gifts (gift_id, sender_tg_id, recipient_tg_id, selected_months, expiry_time, gift_link, created_at, is_used) - VALUES ($1, $2, NULL, $3, $4, $5, $6, FALSE) - """, - gift_id, - sender_tg_id, - selected_months, - expiry_time, - gift_link, - datetime.utcnow(), - ) - - if result: - logger.info(f"Подарок с ID {gift_id} успешно добавлен в базу данных.") - return True - else: - logger.error(f"Не удалось добавить подарок с ID {gift_id} в базу данных.") - return False - except Exception as e: - logger.error(f"Ошибка при сохранении подарка с ID {gift_id} в базе данных: {e}") - return False - - finally: - if conn is not None and session is None: - await conn.close() - - -async def get_key_details(email, session): - record = await session.fetchrow( - """ - SELECT k.key, k.email, k.expiry_time, k.server_id,k, k.client_id, k.created_at, c.tg_id, c.balance - FROM keys k - JOIN connections c ON k.tg_id = c.tg_id - WHERE k.email = $1 - """, - email, - ) - - if not record: - return None - - cluster_name = record["server_id"] - - moscow_tz = pytz.timezone("Europe/Moscow") - expiry_date = datetime.fromtimestamp(record["expiry_time"] / 1000, tz=moscow_tz) - current_date = datetime.now(moscow_tz) - time_left = expiry_date - current_date - - if time_left.total_seconds() <= 0: - days_left_message = "Ключ истек." - elif time_left.days > 0: - days_left_message = f"Осталось дней: {time_left.days}" - else: - hours_left = time_left.seconds // 3600 - days_left_message = f"Осталось часов: {hours_left}" - - return { - "key": record["key"], - "created_at": record["created_at"], - "expiry_time": record["expiry_time"], - "client_id": record["client_id"], - "expiry_date": expiry_date.strftime("%d %B %Y года %H:%M"), - "days_left_message": days_left_message, - "server_name": cluster_name, - "balance": record["balance"], - "tg_id": record["tg_id"], - "email": record["email"], - } - - -async def delete_key(identifier, session): - """ - Удаляет ключ из базы данных по client_id или tg_id - - Args: - identifier (str): client_id или tg_id для удаления - session: Сессия базы данных - - Raises: - Exception: В случае ошибки при удалении ключа - """ - try: - # Проверяем, является ли идентификатор числом (tg_id) или строкой (client_id) - if identifier.isdigit(): - query = "DELETE FROM keys WHERE tg_id = $1" - else: - query = "DELETE FROM keys WHERE client_id = $1" - - await session.execute(query, identifier) - logger.info(f"Ключ с идентификатором {identifier} успешно удалён") - except Exception as e: - logger.error(f"Ошибка при удалении ключа с идентификатором {identifier} из базы данных: {e}") - - -async def create_server( - cluster_name: str, server_name: str, api_url: str, subscription_url: str, inbound_id: int, session: Any -): - """ - Добавляет новый сервер в базу данных. - - Args: - cluster_name (str): Название кластера - server_name (str): Название сервера - api_url (str): URL API сервера - subscription_url (str): URL подписки - inbound_id (int): ID входящего подключения - session (Any): Сессия базы данных - - Raises: - Exception: В случае ошибки при добавлении сервера - """ - try: - await session.execute( - """ - INSERT INTO servers (cluster_name, server_name, api_url, subscription_url, inbound_id) - VALUES ($1, $2, $3, $4, $5) - """, - cluster_name, - server_name, - api_url, - subscription_url, - inbound_id, - ) - logger.info(f"Сервер {server_name} успешно добавлен в кластер {cluster_name}") - except Exception as e: - logger.error(f"Ошибка при добавлении сервера {server_name} в кластер {cluster_name}: {e}") - raise - - -async def delete_server(server_name: str, session: Any): - """ - Удаляет сервер из базы данных по его названию. - - Args: - server_name (str): Название сервера для удаления - session (Any): Сессия базы данных - - Raises: - Exception: В случае ошибки при удалении сервера - """ - try: - await session.execute( - """ - DELETE FROM servers WHERE server_name = $1 - """, - server_name, - ) - logger.info(f"Сервер {server_name} успешно удалён из базы данных") - except Exception as e: - logger.error(f"Ошибка при удалении сервера {server_name} из базы данных: {e}") - raise - - -async def create_coupon_usage(coupon_id: int, user_id: int, session: Any): - """ - Создаёт запись об использовании купона в базе данных. - - Args: - coupon_id (int): ID купона - user_id (int): ID пользователя - session (Any): Сессия базы данных - - Raises: - Exception: В случае ошибки при создании записи - """ - try: - await session.execute( - """ - INSERT INTO coupon_usages (coupon_id, user_id, used_at) - VALUES ($1, $2, $3) - """, - coupon_id, - user_id, - datetime.utcnow(), - ) - logger.info(f"Создана запись об использовании купона {coupon_id} пользователем {user_id}") - except Exception as e: - logger.error(f"Ошибка при создании записи об использовании купона {coupon_id} пользователем {user_id}: {e}") - raise - - -async def check_coupon_usage(coupon_id: int, user_id: int, session: Any) -> bool: - """ - Проверяет, использовал ли пользователь данный купон. - - Args: - coupon_id (int): ID купона для проверки - user_id (int): ID пользователя для проверки - session (Any): Сессия базы данных - - Returns: - bool: True если купон уже использован, False если нет - - Raises: - Exception: В случае ошибки при выполнении запроса - """ - try: - result = await session.fetchrow( - """ - SELECT 1 FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2 - """, - coupon_id, - user_id, - ) - return result is not None - except Exception as e: - logger.error(f"Ошибка при проверке использования купона {coupon_id} пользователем {user_id}: {e}") - raise - - -async def update_coupon_usage_count(coupon_id: int, session: Any): - """ - Обновляет счетчик использования купона и его статус. - - Args: - coupon_id (int): ID купона для обновления - session (Any): Сессия базы данных - - Raises: - Exception: В случае ошибки при обновлении данных купона - """ - try: - await session.execute( - """ - UPDATE coupons - SET usage_count = usage_count + 1, - is_used = CASE WHEN usage_count + 1 >= usage_limit AND usage_limit > 0 THEN TRUE ELSE FALSE END - WHERE id = $1 - """, - coupon_id, - ) - logger.info(f"Успешно обновлен счетчик использования купона {coupon_id}") - except Exception as e: - logger.error(f"Ошибка при обновлении счетчика использования купона {coupon_id}: {e}") - raise - - -async def get_last_payments(tg_id: int, session: Any): - """ - Получает последние 3 платежа пользователя. - - Args: - tg_id (int): Telegram ID пользователя - session (Any): Сессия базы данных - - Returns: - list: Список последних платежей пользователя - - Raises: - Exception: В случае ошибки при выполнении запроса - """ - try: - records = await session.fetch( - """ - SELECT amount, payment_system, status, created_at - FROM payments - WHERE tg_id = $1 - ORDER BY created_at DESC - LIMIT 3 - """, - tg_id, - ) - logger.info(f"Успешно получены последние платежи для пользователя {tg_id}") - return records - except Exception as e: - logger.error(f"Ошибка при получении последних платежей для пользователя {tg_id}: {e}") - raise + await conn.close() \ No newline at end of file diff --git a/handlers/admin/admin_bans.py b/handlers/admin/admin_bans.py index 9ab8e7cf..833f3084 100644 --- a/handlers/admin/admin_bans.py +++ b/handlers/admin/admin_bans.py @@ -1,7 +1,7 @@ from typing import Any from aiogram import F, Router -from aiogram.types import CallbackQuery, BufferedInputFile +from aiogram.types import BufferedInputFile, CallbackQuery from database import delete_user_data from filters.admin import IsAdminFilter diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index b65ea5e4..52c10a4e 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -4,9 +4,9 @@ from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from database import delete_coupon, get_all_coupons, create_coupon +from database import create_coupon, delete_coupon, get_all_coupons from filters.admin import IsAdminFilter -from keyboards.admin.coupons_kb import build_coupons_kb, build_coupons_list_kb, AdminCouponDeleteCallback +from keyboards.admin.coupons_kb import AdminCouponDeleteCallback, build_coupons_kb, build_coupons_list_kb from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb from logger import logger diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index eb9eb744..f3d76f87 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -5,7 +5,7 @@ from aiogram.types import CallbackQuery from bot import version from filters.admin import IsAdminFilter -from keyboards.admin.panel_kb import build_panel_kb, AdminPanelCallback, build_management_kb +from keyboards.admin.panel_kb import AdminPanelCallback, build_management_kb, build_panel_kb router = Router() diff --git a/handlers/admin/admin_restart.py b/handlers/admin/admin_restart.py index 58c1a115..d2501758 100644 --- a/handlers/admin/admin_restart.py +++ b/handlers/admin/admin_restart.py @@ -4,7 +4,7 @@ from aiogram import F, Router from aiogram.types import CallbackQuery from filters.admin import IsAdminFilter -from keyboards.admin.panel_kb import build_restart_kb, AdminPanelCallback, build_admin_back_kb +from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb, build_restart_kb router = Router() diff --git a/handlers/admin/admin_sender.py b/handlers/admin/admin_sender.py index 70563437..0be8e6e8 100644 --- a/handlers/admin/admin_sender.py +++ b/handlers/admin/admin_sender.py @@ -8,7 +8,7 @@ from aiogram.types import CallbackQuery from filters.admin import IsAdminFilter from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb -from keyboards.admin.sender_kb import build_sender_kb, AdminSenderCallback +from keyboards.admin.sender_kb import AdminSenderCallback, build_sender_kb from logger import logger router = Router() diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index 51257e9a..2969e83d 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -6,15 +6,15 @@ from py3xui import AsyncApi from backup import create_backup_and_send_to_admins from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL -from database import create_server, check_unique_server_name, delete_server, get_keys_by_server, get_servers +from database import check_unique_server_name, delete_server, get_servers from filters.admin import IsAdminFilter from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb from keyboards.admin.servers_kb import ( - build_manage_server_kb, + AdminServerEditorCallback, + build_clusters_editor_kb, build_delete_server_kb, build_manage_cluster_kb, - build_clusters_editor_kb, - AdminServerEditorCallback, + build_manage_server_kb, ) router = Router() diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index 2d470b56..0538d245 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -16,8 +16,8 @@ from database import ( get_key_details, get_keys, get_servers, - update_trial, update_key_expiry, + update_trial, ) from filters.admin import IsAdminFilter from handlers.keys.key_utils import ( diff --git a/handlers/admin/admin_users.py b/handlers/admin/admin_users.py index 4601d646..fe76bec5 100644 --- a/handlers/admin/admin_users.py +++ b/handlers/admin/admin_users.py @@ -21,17 +21,17 @@ from handlers.keys.key_utils import ( from handlers.utils import sanitize_key_name from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb from keyboards.admin.users_kb import ( - build_user_edit_kb, - build_key_edit_kb, - build_key_delete_kb, - build_user_delete_kb, AdminUserEditorCallback, - build_editor_kb, - build_users_balance_kb, - build_users_balance_change_kb, - build_user_key_kb, - build_users_key_expiry_kb, AdminUserKeyEditorCallback, + build_editor_kb, + build_key_delete_kb, + build_key_edit_kb, + build_user_delete_kb, + build_user_edit_kb, + build_user_key_kb, + build_users_balance_change_kb, + build_users_balance_kb, + build_users_key_expiry_kb, build_users_key_show_kb, ) from logger import logger @@ -428,7 +428,7 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se text = f"✅ Время действия ключа изменено на {message.text}" await change_expiry_time(expiry_time, email, session) except ValueError: - text = f"🚫 Пожалуйста, используйте корректный формат даты!" + text = "🚫 Пожалуйста, используйте корректный формат даты!" except Exception as e: text = f"❗ Произошла ошибка во время изменения времени действия ключа: {e}" diff --git a/handlers/coupons.py b/handlers/coupons.py index b652cf1c..0a125da2 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -1,4 +1,3 @@ -from datetime import datetime from typing import Any from aiogram import F, Router, types diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 8264ee79..3330f2da 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -24,10 +24,10 @@ from config import ( USE_NEW_PAYMENT_FLOW, ) from database import ( + create_temporary_data, get_balance, get_key_details, get_trial, - create_temporary_data, store_key, update_balance, ) diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index 80461348..8de0861b 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -4,7 +4,7 @@ from typing import Any from py3xui import AsyncApi from client import ClientConfig, add_client, delete_client, extend_client_key -from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, SUPERNODE, TOTAL_GB, PUBLIC_LINK +from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB from database import get_servers, store_key from handlers.utils import get_least_loaded_cluster from logger import logger diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 7bbf3eb8..5586c164 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -10,7 +10,6 @@ import pytz from aiogram import F, Router, types from aiogram.types import BufferedInputFile, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from handlers.payments.yookassa_pay import process_custom_amount_input from bot import bot from config import ( @@ -29,14 +28,13 @@ from config import ( ) from database import ( check_server_name_by_cluster, + create_temporary_data, delete_key, get_balance, get_key_details, get_keys, get_keys_by_server, get_servers, - create_temporary_data, - store_key, update_balance, update_key_expiry, ) @@ -55,6 +53,7 @@ from handlers.keys.key_utils import ( update_subscription, ) from handlers.payments.robokassa_pay import handle_custom_amount_input +from handlers.payments.yookassa_pay import process_custom_amount_input from handlers.texts import ( DISCOUNTS, KEY_NOT_FOUND_MSG, diff --git a/handlers/notifications.py b/handlers/notifications.py index 35c7c2c6..614ee497 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -23,9 +23,9 @@ from config import ( TRIAL_TIME, ) from database import ( - create_blocked_user, add_notification, check_notification_time, + create_blocked_user, delete_key, get_balance, get_servers, diff --git a/keyboards/admin/bans_kb.py b/keyboards/admin/bans_kb.py index 0c5554d4..3c5c5145 100644 --- a/keyboards/admin/bans_kb.py +++ b/keyboards/admin/bans_kb.py @@ -1,7 +1,7 @@ from aiogram.types import InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder -from keyboards.admin.panel_kb import build_admin_back_btn, AdminPanelCallback +from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn def build_bans_kb() -> InlineKeyboardMarkup: diff --git a/keyboards/admin/panel_kb.py b/keyboards/admin/panel_kb.py index 6b16151f..2c23381d 100644 --- a/keyboards/admin/panel_kb.py +++ b/keyboards/admin/panel_kb.py @@ -1,5 +1,5 @@ from aiogram.filters.callback_data import CallbackData -from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder diff --git a/keyboards/admin/stats_kb.py b/keyboards/admin/stats_kb.py index 3a6e4092..b8046593 100644 --- a/keyboards/admin/stats_kb.py +++ b/keyboards/admin/stats_kb.py @@ -1,7 +1,7 @@ from aiogram.types import InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder -from keyboards.admin.panel_kb import build_admin_back_btn, AdminPanelCallback +from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn def build_stats_kb() -> InlineKeyboardMarkup: diff --git a/keyboards/admin/users_kb.py b/keyboards/admin/users_kb.py index 78378f9d..54d056a9 100644 --- a/keyboards/admin/users_kb.py +++ b/keyboards/admin/users_kb.py @@ -143,7 +143,7 @@ def build_user_delete_kb(tg_id: int): def build_user_key_kb(tg_id: int, email: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( - text=f"🔙 Назад", callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack() + text="🔙 Назад", callback_data=AdminUserEditorCallback(action="users_key_edit", tg_id=tg_id, data=email).pack() ) builder.adjust(1) return builder.as_markup() diff --git a/servers.py b/servers.py index 521b55f1..6d29431c 100644 --- a/servers.py +++ b/servers.py @@ -9,7 +9,7 @@ from ping3 import ping from bot import bot from config import ADMIN_ID, DATABASE_URL, PING_TIME -from database import create_server, check_unique_server_name, get_servers +from database import check_unique_server_name, create_server, get_servers from logger import logger try: From 6bffaab94f4728ae1c3fe42d0ab5bd0780a8eb0c Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 29 Jan 2025 04:15:40 +0000 Subject: [PATCH 41/49] Auto-format code with Ruff using pyproject.toml --- database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database.py b/database.py index 2492ddc6..ea1b5cab 100644 --- a/database.py +++ b/database.py @@ -1567,4 +1567,4 @@ async def get_all_keys(session: Any = None): raise finally: if conn is not None and session is None: - await conn.close() \ No newline at end of file + await conn.close() From 7327a967308bac744efa6fb426ff5787ff759025 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Wed, 29 Jan 2025 07:16:21 +0300 Subject: [PATCH 42/49] Improve type hinting for database backup function Update return type annotation for `_create_database_backup()` to use explicit tuple type syntax, enhancing type clarity and compatibility with modern Python type hinting practices. --- backup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backup.py b/backup.py index 51c6636b..c0d5ffe0 100644 --- a/backup.py +++ b/backup.py @@ -29,7 +29,7 @@ async def backup_database() -> Exception | None: return e -def _create_database_backup() -> (str | None, Exception | None): +def _create_database_backup() -> tuple[str | None, Exception | None]: date_formatted = datetime.now().strftime("%Y-%m-%d-%H%M%S") if not os.path.exists(BACK_DIR): From 32ba7b0a6728ee1abcfcf6658a2f8a65e514b2b4 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Wed, 29 Jan 2025 07:17:53 +0300 Subject: [PATCH 43/49] Minor whitespace adjustment in database connection closing --- database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database.py b/database.py index ea1b5cab..2492ddc6 100644 --- a/database.py +++ b/database.py @@ -1567,4 +1567,4 @@ async def get_all_keys(session: Any = None): raise finally: if conn is not None and session is None: - await conn.close() + await conn.close() \ No newline at end of file From de45f5d377f84c7fed2ba6ce56c0e4c9a412e17a Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Wed, 29 Jan 2025 04:18:09 +0000 Subject: [PATCH 44/49] Auto-format code with Ruff using pyproject.toml --- database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database.py b/database.py index 2492ddc6..ea1b5cab 100644 --- a/database.py +++ b/database.py @@ -1567,4 +1567,4 @@ async def get_all_keys(session: Any = None): raise finally: if conn is not None and session is None: - await conn.close() \ No newline at end of file + await conn.close() From 83870a20225550613c68132bf642fee7ede92513 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 30 Jan 2025 22:35:09 +0300 Subject: [PATCH 45/49] Refactor and improve security with cryptographically secure random generation - Replace `random` module with `secrets` for cryptographically secure random choices - Update type hints for async API methods in client.py - Optimize file reading with aiofiles in multiple handlers - Improve error handling and logging in various modules - Update Python target version to 3.12 in pyproject.toml - Remove unused imports and improve code quality --- backup.py | 3 ++- client.py | 6 +++--- handlers/admin/admin_sender.py | 4 ++-- handlers/admin/admin_servers.py | 20 +++++++++++------- handlers/admin/admin_user_editor.py | 10 ++++----- handlers/admin/admin_users.py | 15 +++++++------- handlers/captcha.py | 7 ++++--- handlers/instructions/instructions.py | 1 - handlers/keys/keys.py | 5 ++--- handlers/notifications.py | 30 ++++++++++++++++----------- handlers/utils.py | 4 +++- middlewares/delete.py | 5 +++-- pyproject.toml | 4 ++-- servers.py | 2 +- 14 files changed, 64 insertions(+), 52 deletions(-) diff --git a/backup.py b/backup.py index c0d5ffe0..8550e619 100644 --- a/backup.py +++ b/backup.py @@ -97,9 +97,10 @@ async def create_backup_and_send_to_admins(xui) -> None: async def _send_backup_to_admins(backup_file_path: str) -> None: try: - from bot import bot import aiofiles + from bot import bot + async with aiofiles.open(backup_file_path, "rb") as backup_file: backup_data = await backup_file.read() backup_input_file = BufferedInputFile(file=backup_data, filename=os.path.basename(backup_file_path)) diff --git a/client.py b/client.py index 984a662a..219ccca5 100644 --- a/client.py +++ b/client.py @@ -23,7 +23,7 @@ class ClientConfig: sub_id: str -async def add_client(xui: py3xui.API, config: ClientConfig) -> dict[str, Any]: +async def add_client(xui: py3xui.AsyncApi, config: ClientConfig) -> dict[str, Any]: """ Добавляет клиента на сервер через 3x-ui. @@ -66,7 +66,7 @@ async def add_client(xui: py3xui.API, config: ClientConfig) -> dict[str, Any]: async def extend_client_key( - xui: py3xui.API, inbound_id: int, email: str, new_expiry_time: int, client_id: str, total_gb: int, sub_id: str + xui: py3xui.AsyncApi, inbound_id: int, email: str, new_expiry_time: int, client_id: str, total_gb: int, sub_id: str ) -> bool | None: """ Обновляет срок действия ключа клиента. @@ -117,7 +117,7 @@ async def extend_client_key( async def delete_client( - xui: py3xui.API, + xui: py3xui.AsyncApi, inbound_id: int, email: str, client_id: str, diff --git a/handlers/admin/admin_sender.py b/handlers/admin/admin_sender.py index 0be8e6e8..4e8a92cf 100644 --- a/handlers/admin/admin_sender.py +++ b/handlers/admin/admin_sender.py @@ -85,8 +85,8 @@ async def handle_message_input(message: types.Message, state: FSMContext, sessio try: await message.bot.send_message(chat_id=tg_id, text=text_message) success_count += 1 - except Exception: - pass + except Exception as e: + logger.error(e) text = ( f"📤 Рассылка завершена!" diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index 2969e83d..9632b424 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -1,3 +1,5 @@ +from typing import Any + import asyncpg from aiogram import F, Router, types from aiogram.fsm.context import FSMContext @@ -33,7 +35,7 @@ class AdminServersEditor(StatesGroup): IsAdminFilter(), ) async def handle_servers(callback_query: types.CallbackQuery): - servers = await get_servers_from_db() + servers = await get_servers() text = ( "🔧 Управление кластерами\n\n" @@ -227,8 +229,7 @@ async def handle_inbound_id_input(message: types.Message, state: FSMContext): @router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_manage"), IsAdminFilter()) async def handle_clusters_manage( - callback_query: types.CallbackQuery, - callback_data: AdminServerEditorCallback, + callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any ): cluster_name = callback_data.data @@ -242,7 +243,9 @@ async def handle_clusters_manage( @router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_availability"), IsAdminFilter()) -async def handle_servers_availability(callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback): +async def handle_servers_availability( + callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any +): cluster_name = callback_data.data servers = await get_servers(session) @@ -279,7 +282,7 @@ async def handle_servers_availability(callback_query: types.CallbackQuery, callb @router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_manage"), IsAdminFilter()) async def handle_servers_manage(callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback): server_name = callback_data.data - servers = await get_servers_from_db() + servers = await get_servers() cluster_name, server = next( ((c, s) for c, cs in servers.items() for s in cs if s["server_name"] == server_name), (None, None) @@ -316,7 +319,9 @@ async def handle_servers_delete(callback_query: types.CallbackQuery, callback_da @router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_delete_confirm"), IsAdminFilter()) -async def handle_servers_delete_confirm(callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback): +async def handle_servers_delete_confirm( + callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any +): server_name = callback_data.data await delete_server(server_name, session) @@ -350,8 +355,7 @@ async def handle_servers_add( @router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_backup"), IsAdminFilter()) async def handle_clusters_backup( - callback_query: types.CallbackQuery, - callback_data: AdminServerEditorCallback, + callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any ): cluster_name = callback_data.data diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index 0538d245..4d4ff573 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -11,6 +11,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder from config import TOTAL_GB from database import ( + delete_key, delete_user_data, get_client_id_by_email, get_key_details, @@ -22,7 +23,6 @@ from database import ( from filters.admin import IsAdminFilter from handlers.keys.key_utils import ( delete_key_from_cluster, - delete_key_from_db, renew_key_in_cluster, ) from handlers.utils import sanitize_key_name @@ -415,7 +415,7 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se async def update_key_on_all_servers(): tasks = [] for cluster_name, cluster_servers in clusters.items(): - for server in cluster_servers: + for _server in cluster_servers: tasks.append( asyncio.create_task( renew_key_in_cluster( @@ -493,12 +493,12 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery, s async def delete_key_from_servers(email, client_id): tasks = [] for cluster_name, cluster_servers in clusters.items(): - for server in cluster_servers: + for _server in cluster_servers: tasks.append(delete_key_from_cluster(cluster_name, email, client_id)) await asyncio.gather(*tasks) await delete_key_from_servers(email, client_id) - await delete_key_from_db(client_id, session) + await delete_key(client_id, session) await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) else: @@ -570,7 +570,7 @@ async def delete_user(callback_query: types.CallbackQuery, session: Any): tasks = [] for email, client_id in key_records: servers = await get_servers(session) - for cluster_id, cluster in servers.items(): + for cluster_id, _cluster in servers.items(): tasks.append(delete_key_from_cluster(cluster_id, email, client_id)) await asyncio.gather(*tasks) except Exception as e: diff --git a/handlers/admin/admin_users.py b/handlers/admin/admin_users.py index fe76bec5..8ad42bea 100644 --- a/handlers/admin/admin_users.py +++ b/handlers/admin/admin_users.py @@ -10,11 +10,10 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery from config import TOTAL_GB -from database import delete_user_data, get_client_id_by_email, get_servers_from_db, restore_trial, update_key_expiry +from database import delete_key, delete_user_data, get_client_id_by_email, get_servers, update_key_expiry, update_trial from filters.admin import IsAdminFilter from handlers.keys.key_utils import ( delete_key_from_cluster, - delete_key_from_db, renew_key_in_cluster, update_subscription, ) @@ -168,7 +167,7 @@ async def handle_trial_restore( ): tg_id = callback_data.tg_id - await restore_trial(tg_id, session) + await update_trial(tg_id, 0, session) await callback_query.message.edit_text(text="✅ Триал успешно восстановлен!", reply_markup=build_editor_kb(tg_id)) @@ -479,7 +478,7 @@ async def handle_delete_key_confirm( if record: client_id = record["client_id"] - clusters = await get_servers_from_db() + clusters = await get_servers() async def delete_key_from_servers(): tasks = [] @@ -489,7 +488,7 @@ async def handle_delete_key_confirm( await asyncio.gather(*tasks) await delete_key_from_servers() - await delete_key_from_db(client_id, session) + await delete_key(client_id, session) await callback_query.message.edit_text(text="✅ Ключ успешно удален.", reply_markup=kb) else: @@ -515,8 +514,8 @@ async def handle_delete_user_confirm( try: tasks = [] for email, client_id in key_records: - servers = await get_servers_from_db() - for cluster_id, cluster in servers.items(): + servers = await get_servers() + for cluster_id, _cluster in servers.items(): tasks.append(delete_key_from_cluster(cluster_id, email, client_id)) await asyncio.gather(*tasks) except Exception as e: @@ -619,7 +618,7 @@ async def change_expiry_time(expiry_time: int, email: str, session: Any) -> Exce if not server_id: return ValueError(f"User with client_id {server_id} was not found") - clusters = await get_servers_from_db() + clusters = await get_servers() async def update_key_on_all_servers(): tasks = [ diff --git a/handlers/captcha.py b/handlers/captcha.py index fd67d534..5730f45f 100644 --- a/handlers/captcha.py +++ b/handlers/captcha.py @@ -1,4 +1,5 @@ import random +import secrets from typing import Any from aiogram import F, Router @@ -7,7 +8,6 @@ from aiogram.types import CallbackQuery, Message from aiogram.utils.keyboard import InlineKeyboardBuilder from config import CAPTCHA_EMOJIS -from handlers.start import start_command from logger import logger router = Router() @@ -27,7 +27,7 @@ async def generate_captcha(message: Message, state: FSMContext): } """ # Выбираем случайный эмодзи и его описание из конфига - correct_emoji, correct_text = random.choice(list(CAPTCHA_EMOJIS.items())) + correct_emoji, correct_text = secrets.choice(list(CAPTCHA_EMOJIS.items())) # Получаем 3 случайных неправильных эмодзи wrong_emojis = random.sample([e for e in CAPTCHA_EMOJIS.keys() if e != correct_emoji], 3) @@ -54,7 +54,8 @@ async def generate_captcha(message: Message, state: FSMContext): @router.callback_query(F.data.startswith("captcha_")) async def check_captcha(callback: CallbackQuery, state: FSMContext, session: Any, admin: bool): - """Проверяет ответ пользователя на капчу""" + from handlers.start import start_command + selected_emoji = callback.data.split("captcha_")[1] state_data = await state.get_data() correct_emoji = state_data.get("correct_emoji") diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index dd373ccb..89053024 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -57,7 +57,6 @@ async def send_instructions( @router.callback_query(F.data.startswith("connect_pc|")) async def process_connect_pc(callback_query: types.CallbackQuery, session: Any): - tg_id = callback_query.message.chat.id key_name = callback_query.data.split("|")[1] record = await get_key_details(key_name, session) diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 5586c164..018d3f1e 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -48,7 +48,6 @@ from handlers.buttons.add_subscribe import ( ) from handlers.keys.key_utils import ( delete_key_from_cluster, - delete_key_from_db, renew_key_in_cluster, update_subscription, ) @@ -354,7 +353,7 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery, s async def delete_key_from_servers(): try: tasks = [] - for cluster_id, cluster in servers.items(): + for cluster_id, _cluster in servers.items(): tasks.append(delete_key_from_cluster(cluster_id, email, client_id)) await asyncio.gather(*tasks) @@ -364,7 +363,7 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery, s asyncio.create_task(delete_key_from_servers()) - await delete_key_from_db(client_id, session) + await delete_key(client_id, session) else: response_message = "Ключ не найден или уже удален." diff --git a/handlers/notifications.py b/handlers/notifications.py index 614ee497..f99cc6c3 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -2,6 +2,7 @@ import asyncio import os from datetime import datetime, timedelta +import aiofiles import asyncpg import pytz from aiogram import Bot, Router, types @@ -169,10 +170,11 @@ async def process_10h_record(record, bot, conn): ) if os.path.isfile(image_path): - with open(image_path, "rb") as image_file: + async with aiofiles.open(image_path, "rb") as image_file: + image_data = await image_file.read() await bot.send_photo( tg_id, - photo=BufferedInputFile(image_file.read(), filename="notify_10h.jpg"), + photo=BufferedInputFile(image_data, filename="notify_10h.jpg"), caption=KEY_RENEWED.format(email=email), reply_markup=keyboard, ) @@ -258,10 +260,11 @@ async def process_24h_record(record, bot, conn): ) if os.path.isfile(image_path): - with open(image_path, "rb") as image_file: + async with aiofiles.open(image_path, "rb") as image_file: + image_data = await image_file.read() await bot.send_photo( tg_id, - photo=BufferedInputFile(image_file.read(), filename="notify_24h.jpg"), + photo=BufferedInputFile(image_data, filename="notify_24h.jpg"), caption=KEY_RENEWED.format(email=email), reply_markup=keyboard, ) @@ -286,10 +289,11 @@ async def send_renewal_notification(bot, tg_id, email, message, conn, client_id, image_path = os.path.join("img", "notify_24h.jpg") if os.path.isfile(image_path): - with open(image_path, "rb") as image_file: + async with aiofiles.open(image_path, "rb") as image_file: + image_data = await image_file.read() await bot.send_photo( tg_id, - photo=BufferedInputFile(image_file.read(), filename="notify_24h.jpg"), + photo=BufferedInputFile(image_data, filename="notify_24h.jpg"), caption=message, reply_markup=keyboard.as_markup(), ) @@ -298,7 +302,7 @@ async def send_renewal_notification(bot, tg_id, email, message, conn, client_id, logger.info(f"Уведомление отправлено пользователю {tg_id}.") - await conn.execute(f"UPDATE keys SET {flag} = TRUE WHERE client_id = $1", client_id) + await conn.execute("UPDATE keys SET notified_24h = $1 WHERE client_id = $2", flag, client_id) except Exception as e: logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}") @@ -456,10 +460,11 @@ async def process_key(record, bot, conn): try: if os.path.isfile(image_path): - with open(image_path, "rb") as image_file: + async with aiofiles.open(image_path, "rb") as image_file: + image_data = await image_file.read() await bot.send_photo( tg_id, - photo=BufferedInputFile(image_file.read(), filename="notify_expired.jpg"), + photo=BufferedInputFile(image_data, filename="notify_expired.jpg"), caption=KEY_RENEWED.format(email=email), reply_markup=keyboard, ) @@ -473,10 +478,11 @@ async def process_key(record, bot, conn): message_expired = f"Ваша подписка {email} истекла. Пополните баланс для продления." try: if os.path.isfile(image_path): - with open(image_path, "rb") as image_file: + async with aiofiles.open(image_path, "rb") as image_file: + image_data = await image_file.read() await bot.send_photo( tg_id, - photo=BufferedInputFile(image_file.read(), filename="notify_expired.jpg"), + photo=BufferedInputFile(image_data, filename="notify_expired.jpg"), caption=message_expired, reply_markup=keyboard, ) @@ -511,7 +517,7 @@ async def process_key(record, bot, conn): async def check_online_users(): servers = await get_servers() - for cluster_id, cluster in servers.items(): + for _cluster_id, cluster in servers.items(): for server_id, server in enumerate(cluster): xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD) await xui.login() diff --git a/handlers/utils.py b/handlers/utils.py index 29d525f7..57339821 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -1,6 +1,8 @@ import json import random import re +import secrets +import string import aiohttp import asyncpg @@ -49,7 +51,7 @@ def generate_random_email(length: int = 6) -> str: Returns: str: Сгенерированная случайная строка. """ - return "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=length)) + return "".join(secrets.choice(string.ascii_lowercase + string.digits) for _ in range(length)) if length > 0 else "" async def get_least_loaded_cluster() -> str: diff --git a/middlewares/delete.py b/middlewares/delete.py index af20634f..b305c7bb 100644 --- a/middlewares/delete.py +++ b/middlewares/delete.py @@ -9,6 +9,7 @@ from keyboards.admin.panel_kb import AdminPanelCallback from keyboards.admin.sender_kb import AdminSenderCallback from keyboards.admin.servers_kb import AdminServerEditorCallback from keyboards.admin.users_kb import AdminUserEditorCallback, AdminUserKeyEditorCallback +from logger import logger pass_callbacks = [ AdminPanelCallback, @@ -31,8 +32,8 @@ class DeleteMessageMiddleware(BaseMiddleware): if not event.text or not event.text.startswith("/start"): try: await event.bot.delete_message(event.chat.id, event.message_id - 1) - except Exception: - pass + except Exception as e: + logger.error(e) await event.delete() if isinstance(event, CallbackQuery): diff --git a/pyproject.toml b/pyproject.toml index 69cbd0f1..634f9533 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [tool.ruff] line-length = 120 -target-version = "py310" +target-version = "py312" [tool.ruff.lint] select = ["E", "F", "W", "I", "N", "UP", "ANN", "ASYNC", "S", "BLE", "FBT", "B", "A", "C4", "DTZ", "T10", "ISC", "ICN", "G", "PIE"] -ignore = ["ANN101", "ANN102", "S101",'ANN201','ANN001','BLE001'] +ignore = ["ANN101", "ANN102", "S101",'ANN201','ANN001','BLE001','W291','ANN401','DTZ003','DTZ005','F401','FBT002','FBT001','FBT003','A005','E501','UP017','DTZ004','W293','ANN202','DTZ007'] exclude = [ ".git", "venv", diff --git a/servers.py b/servers.py index 6d29431c..e1f35d53 100644 --- a/servers.py +++ b/servers.py @@ -33,7 +33,7 @@ async def sync_servers_with_db(): logger.info("Подключение к базе данных для синхронизации серверов успешно.") for cluster_name, servers in CLUSTERS.items(): - for server_key, server_info in servers.items(): + for _server_key, server_info in servers.items(): exists = await check_unique_server_name(server_info["name"], conn, cluster_name) if not exists: From f0e78baa19ea02f5c42fffdd06c90a4cd07fd303 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 30 Jan 2025 23:36:02 +0300 Subject: [PATCH 46/49] Remove unused is_bot_blocked function from notifications handler --- handlers/notifications.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/handlers/notifications.py b/handlers/notifications.py index f9eb93c2..3bdb4496 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -89,19 +89,6 @@ async def notify_expiring_keys(bot: Bot): logger.info("Соединение с базой данных закрыто.") -async def is_bot_blocked(bot: Bot, chat_id: int) -> bool: - if DEV_MODE: - return False - try: - member = await bot.get_chat_member(chat_id, bot.id) - blocked = member.status == "left" - logger.info(f"Статус бота для пользователя {chat_id}: {'заблокирован' if blocked else 'активен'}") - return blocked - except Exception as e: - logger.warning(f"Не удалось проверить статус бота для пользователя {chat_id}: {e}") - return False - - async def notify_10h_keys( bot: Bot, conn: asyncpg.Connection, From 1fad6cae3c9d874fa443d195ff56cd47aebb8220 Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 30 Jan 2025 20:36:20 +0000 Subject: [PATCH 47/49] Auto-format code with Ruff using pyproject.toml --- handlers/notifications.py | 35 ++++++++++++++--------------------- handlers/utils.py | 9 +++++---- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/handlers/notifications.py b/handlers/notifications.py index 3bdb4496..0a984556 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -358,6 +358,7 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): await asyncio.sleep(1) + async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: float): logger.info("Проверка подписок, срок действия которых скоро истекает...") @@ -394,16 +395,17 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: for record in expired_keys: try: await delete_key_from_cluster( - cluster_id=record["server_id"], - email=record["email"], - client_id=record["client_id"] + cluster_id=record["server_id"], email=record["email"], client_id=record["client_id"] ) await delete_key(record["client_id"], conn) - logger.info(f"Ключ {record['client_id']} удалён" + - (f" после задержки {DELETE_KEYS_DELAY} сек." if DELETE_KEYS_DELAY > 0 else "")) + logger.info( + f"Ключ {record['client_id']} удалён" + + (f" после задержки {DELETE_KEYS_DELAY} сек." if DELETE_KEYS_DELAY > 0 else "") + ) except Exception as e: logger.error(f"Ошибка при удалении ключа {record['client_id']}: {e}") + async def process_key(record, bot, conn): tg_id = record["tg_id"] client_id = record["client_id"] @@ -425,15 +427,9 @@ async def process_key(record, bot, conn): keyboard = InlineKeyboardBuilder() if DELETE_KEYS_DELAY > 0: - keyboard.row(types.InlineKeyboardButton( - text="🔄 Продлить", - callback_data=f"renew_key|{email}" - )) - - keyboard.row(types.InlineKeyboardButton( - text="👤 Личный кабинет", - callback_data="profile" - )) + keyboard.row(types.InlineKeyboardButton(text="🔄 Продлить", callback_data=f"renew_key|{email}")) + + keyboard.row(types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) image_path = os.path.join("img", "notify_expired.jpg") @@ -483,7 +479,7 @@ async def process_key(record, bot, conn): time_since_expiry = current_time_utc - expiry_time if time_since_expiry <= EXPIRED_KEYS_CHECK_INTERVAL * 1000: message_expired = f"Ваша подписка {email} истекла. Пополните баланс для продления." - + if DELETE_KEYS_DELAY > 0: time_until_deletion = format_time_until_deletion(DELETE_KEYS_DELAY) if time_until_deletion != "0 минут": @@ -500,11 +496,7 @@ async def process_key(record, bot, conn): reply_markup=keyboard.as_markup(), ) else: - await bot.send_message( - tg_id, - text=message_expired, - reply_markup=keyboard.as_markup() - ) + await bot.send_message(tg_id, text=message_expired, reply_markup=keyboard.as_markup()) logger.info(f"Уведомление об истечении подписки отправлено пользователю {tg_id}.") except Exception as e: logger.error(f"Не удалось отправить уведомление об истечении клиенту {tg_id}: {e}") @@ -513,7 +505,7 @@ async def process_key(record, bot, conn): if AUTO_DELETE_EXPIRED_KEYS: current_time = int(datetime.utcnow().timestamp() * 1000) - + if DELETE_KEYS_DELAY == 0 or current_time >= expiry_time + (DELETE_KEYS_DELAY * 1000): servers = await get_servers(conn) @@ -546,6 +538,7 @@ async def process_key(record, bot, conn): except Exception as e: logger.error(f"Ошибка при обработке ключа для клиента {tg_id}: {e}") + async def check_online_users(): servers = await get_servers() diff --git a/handlers/utils.py b/handlers/utils.py index c0a64b33..e52a17c3 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -108,16 +108,17 @@ async def handle_error(tg_id: int, callback_query: object | None = None, message except Exception as e: logger.error(f"Ошибка при обработке ошибки: {e}") + def format_time_until_deletion(seconds: int) -> str: if seconds <= 0: return "0 минут" - + days = seconds // (3600 * 24) hours = (seconds % (3600 * 24)) // 3600 minutes = (seconds % 3600 + 59) // 60 parts = [] - + if days > 0: if days == 1: parts.append(f"{days} день") @@ -125,7 +126,7 @@ def format_time_until_deletion(seconds: int) -> str: parts.append(f"{days} дня") else: parts.append(f"{days} дней") - + if hours > 0: if hours == 1: parts.append(f"{hours} час") @@ -133,7 +134,7 @@ def format_time_until_deletion(seconds: int) -> str: parts.append(f"{hours} часа") else: parts.append(f"{hours} часов") - + if minutes > 0 and days == 0: if minutes == 1: parts.append("1 минута") From c0251b002174b17a6bc4fc6c4c4383fd27b7c33d Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Thu, 30 Jan 2025 23:36:56 +0300 Subject: [PATCH 48/49] Minor text formatting in captcha generation message --- handlers/captcha.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handlers/captcha.py b/handlers/captcha.py index 5730f45f..ef3bbc30 100644 --- a/handlers/captcha.py +++ b/handlers/captcha.py @@ -47,7 +47,7 @@ async def generate_captcha(message: Message, state: FSMContext): builder.adjust(2, 2) return { - "text": f"🔒 Для подтверждения что вы не робот,\nвыберите кнопку с {correct_text}", + "text": f"🔒 Для подтверждения что вы не робот, выберите кнопку с {correct_text}", "markup": builder.as_markup(), } From c9afe25c5200a757368a8b5443fc06492a16a6ae Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Fri, 31 Jan 2025 00:08:31 +0300 Subject: [PATCH 49/49] Remove check_online_users function and minor import reorganization - Removed unused check_online_users function from notifications handler - Reorganized imports in notifications.py and utils.py - Fixed TimeoutError import in subscriptions.py --- handlers/keys/subscriptions.py | 2 +- handlers/notifications.py | 23 +++-------------------- handlers/utils.py | 2 +- 3 files changed, 5 insertions(+), 22 deletions(-) diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index 6203252b..ca2bea1a 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -36,7 +36,7 @@ async def fetch_url_content(url, tg_id): else: logger.error(f"Не удалось получить {url} для tg_id: {tg_id}, статус: {response.status}") return [] - except asyncio.TimeoutError: + except TimeoutError: logger.error(f"Таймаут при получении {url} для tg_id: {tg_id}") return [] except Exception as e: diff --git a/handlers/notifications.py b/handlers/notifications.py index 0a984556..b051e32a 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -10,7 +10,6 @@ from aiogram.exceptions import TelegramForbiddenError from aiogram.types import BufferedInputFile from aiogram.utils.keyboard import InlineKeyboardBuilder from py3xui import AsyncApi -from .utils import format_time_until_deletion from config import ( ADMIN_PASSWORD, @@ -18,12 +17,12 @@ from config import ( AUTO_DELETE_EXPIRED_KEYS, AUTO_RENEW_KEYS, DATABASE_URL, + DELETE_KEYS_DELAY, DEV_MODE, EXPIRED_KEYS_CHECK_INTERVAL, RENEWAL_PLANS, TOTAL_GB, TRIAL_TIME, - DELETE_KEYS_DELAY, ) from database import ( add_notification, @@ -39,6 +38,8 @@ from handlers.keys.key_utils import delete_key_from_cluster, renew_key_in_cluste from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED from logger import logger +from .utils import format_time_until_deletion + router = Router() @@ -74,8 +75,6 @@ async def notify_expiring_keys(bot: Bot): await notify_inactive_trial_users(bot, conn) await asyncio.sleep(0.5) - await check_online_users() - await asyncio.sleep(0.5) await notify_10h_keys(bot, conn, current_time, threshold_time_10h) await asyncio.sleep(0.5) await notify_24h_keys(bot, conn, current_time, threshold_time_24h) @@ -537,19 +536,3 @@ async def process_key(record, bot, conn): except Exception as e: logger.error(f"Ошибка при обработке ключа для клиента {tg_id}: {e}") - - -async def check_online_users(): - servers = await get_servers() - - for _cluster_id, cluster in servers.items(): - for server_id, server in enumerate(cluster): - xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD) - await xui.login() - try: - online_users = len(await xui.client.online()) - logger.info( - f"Сервер '{server['server_name']}' доступен, текущее количество активных пользователей: {online_users}." - ) - except Exception as e: - logger.error(f"Не удалось проверить пользователей на сервере {server_id}: {e}") diff --git a/handlers/utils.py b/handlers/utils.py index e52a17c3..8b4c71a7 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -3,6 +3,7 @@ import random import re import secrets import string +from datetime import datetime, timedelta import aiohttp import asyncpg @@ -11,7 +12,6 @@ from bot import bot from config import DATABASE_URL from database import get_all_keys, get_servers from logger import logger -from datetime import datetime, timedelta async def get_usd_rate():