diff --git a/client.py b/client.py index 96c105f5..c7609ec8 100644 --- a/client.py +++ b/client.py @@ -72,7 +72,14 @@ async def add_client(xui: py3xui.AsyncApi, config: ClientConfig) -> dict[str, An async def extend_client_key( - xui: py3xui.AsyncApi, inbound_id: int, email: str, new_expiry_time: int, client_id: str, total_gb: int, sub_id: str, tg_id: int + xui: py3xui.AsyncApi, + inbound_id: int, + email: str, + new_expiry_time: int, + client_id: str, + total_gb: int, + sub_id: str, + tg_id: int, ) -> bool | None: """ Обновляет срок действия ключа клиента. diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py index 50669db1..4ac02168 100644 --- a/handlers/admin/__init__.py +++ b/handlers/admin/__init__.py @@ -2,22 +2,25 @@ __all__ = ("router",) from aiogram import Router -from .admin_backups import router as backups_router -from .admin_bans import router as bans_router -from .admin_coupons import router as coupons_router -from .admin_panel import router as panel_router -from .admin_restart import router as restart_router -from .admin_sender import router as sender_router -from .admin_servers import router as servers_router -from .admin_stats import router as stats_router -from .admin_users import router as users_router - +from .backups import router as backups_router +from .bans import router as bans_router +from .clusters import router as clusters_router +from .coupons import router as coupons_router +from .management import router as management_router +from .panel import router as panel_router +from .restart import router as restart_router +from .sender import router as sender_router +from .servers import router as servers_router +from .stats import router as stats_router +from .users import router as users_router router = Router(name="admins_main_router") router.include_routers( panel_router, + management_router, servers_router, + clusters_router, users_router, stats_router, backups_router, diff --git a/handlers/admin/backups/__init__.py b/handlers/admin/backups/__init__.py new file mode 100644 index 00000000..5160dafc --- /dev/null +++ b/handlers/admin/backups/__init__.py @@ -0,0 +1,3 @@ +__all__ = ("router",) + +from backups_handler import router diff --git a/handlers/admin/admin_backups.py b/handlers/admin/backups/backups_handler.py similarity index 92% rename from handlers/admin/admin_backups.py rename to handlers/admin/backups/backups_handler.py index 69ad7cc9..35d9c0e8 100644 --- a/handlers/admin/admin_backups.py +++ b/handlers/admin/backups/backups_handler.py @@ -3,7 +3,7 @@ from aiogram.types import CallbackQuery from backup import backup_database from filters.admin import IsAdminFilter -from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb +from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb router = Router() diff --git a/handlers/admin/bans/__init__.py b/handlers/admin/bans/__init__.py new file mode 100644 index 00000000..d03ed5a7 --- /dev/null +++ b/handlers/admin/bans/__init__.py @@ -0,0 +1,3 @@ +__all__ = ("router",) + +from bans_handler import router diff --git a/handlers/admin/admin_bans.py b/handlers/admin/bans/bans_handler.py similarity index 96% rename from handlers/admin/admin_bans.py rename to handlers/admin/bans/bans_handler.py index 44379b5a..28f44213 100644 --- a/handlers/admin/admin_bans.py +++ b/handlers/admin/bans/bans_handler.py @@ -5,8 +5,8 @@ from aiogram.types import BufferedInputFile, CallbackQuery from database import delete_user_data from filters.admin import IsAdminFilter -from keyboards.admin.bans_kb import build_bans_kb -from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb +from keyboard import build_bans_kb +from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb router = Router() diff --git a/keyboards/admin/bans_kb.py b/handlers/admin/bans/keyboard.py similarity index 87% rename from keyboards/admin/bans_kb.py rename to handlers/admin/bans/keyboard.py index 3c5c5145..e967ab04 100644 --- a/keyboards/admin/bans_kb.py +++ b/handlers/admin/bans/keyboard.py @@ -1,7 +1,7 @@ from aiogram.types import InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder -from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn +from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn def build_bans_kb() -> InlineKeyboardMarkup: diff --git a/handlers/admin/clusters/__init__.py b/handlers/admin/clusters/__init__.py new file mode 100644 index 00000000..58b5c5e4 --- /dev/null +++ b/handlers/admin/clusters/__init__.py @@ -0,0 +1,3 @@ +__all__ = ("router",) + +from clusters_handler import router diff --git a/handlers/admin/admin_servers.py b/handlers/admin/clusters/clusters_handler.py similarity index 55% rename from handlers/admin/admin_servers.py rename to handlers/admin/clusters/clusters_handler.py index b1c6a67e..3f292d5d 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/clusters/clusters_handler.py @@ -1,45 +1,39 @@ import asyncio - from typing import Any import asyncpg - from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, Message -from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL from py3xui import AsyncApi from backup import create_backup_and_send_to_admins -from database import check_unique_server_name, delete_server, get_servers +from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL +from database import check_unique_server_name, get_servers from filters.admin import IsAdminFilter from handlers.keys.key_utils import create_key_on_cluster -from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb -from keyboards.admin.servers_kb import ( - AdminServerEditorCallback, +from keyboard import ( build_clusters_editor_kb, - build_delete_server_kb, build_manage_cluster_kb, - build_manage_server_kb, + AdminClusterCallback, ) from logger import logger - +from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb router = Router() -class AdminServersEditor(StatesGroup): +class AdminClusterStates(StatesGroup): waiting_for_cluster_name = State() waiting_for_api_url = State() waiting_for_inbound_id = State() waiting_for_server_name = State() waiting_for_subscription_url = State() - waiting_for_new_domain = State() @router.callback_query( - AdminPanelCallback.filter(F.action == "servers"), + AdminPanelCallback.filter(F.action == "clusters"), IsAdminFilter(), ) async def handle_servers(callback_query: CallbackQuery): @@ -60,26 +54,34 @@ async def handle_servers(callback_query: CallbackQuery): @router.callback_query( - AdminPanelCallback.filter(F.action == "clusters_add"), + AdminClusterCallback.filter(F.action == "add"), IsAdminFilter(), ) async def handle_clusters_add(callback_query: CallbackQuery, state: FSMContext): text = ( "🔧 Введите имя нового кластера:\n\n" - "Имя кластера должно быть уникальным!\n" + "Имя должно быть уникальным!\n" + "Имя не должно превышать 12 символов!\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("clusters")) - await state.set_state(AdminServersEditor.waiting_for_cluster_name) + await state.set_state(AdminClusterStates.waiting_for_cluster_name) -@router.message(AdminServersEditor.waiting_for_cluster_name, IsAdminFilter()) +@router.message(AdminClusterStates.waiting_for_cluster_name, IsAdminFilter()) async def handle_cluster_name_input(message: 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("clusters") + ) + return + + if len(message.text) > 12: + await message.answer( + text="❌ Имя кластера должно превышать 12 символов! Попробуйте снова.", + reply_markup=build_admin_back_kb("clusters"), ) return @@ -94,26 +96,26 @@ async def handle_cluster_name_input(message: Message, state: FSMContext): await message.answer( text=text, - reply_markup=build_admin_back_kb("servers"), + reply_markup=build_admin_back_kb("clusters"), ) - await state.set_state(AdminServersEditor.waiting_for_server_name) + await state.set_state(AdminClusterStates.waiting_for_server_name) -@router.message(AdminServersEditor.waiting_for_server_name, IsAdminFilter()) +@router.message(AdminClusterStates.waiting_for_server_name, IsAdminFilter()) async def handle_server_name_input(message: Message, state: FSMContext, session: Any): if not message.text: await message.answer( - text="❌ Имя сервера не может быть пустым. Попробуйте снова.", reply_markup=build_admin_back_kb("servers") + text="❌ Имя сервера не может быть пустым. Попробуйте снова.", reply_markup=build_admin_back_kb("clusters") ) return server_name = message.text.strip() - if len(server_name) > 14: + if len(server_name) > 12: await message.answer( - text="❌ Имя сервера не должно превышать 10 символов. Попробуйте снова.", - reply_markup=build_admin_back_kb("servers"), + text="❌ Имя сервера не должно превышать 12 символов. Попробуйте снова.", + reply_markup=build_admin_back_kb("clusters"), ) return @@ -123,7 +125,7 @@ async def handle_server_name_input(message: Message, state: FSMContext, session: if not await check_unique_server_name(server_name, session, cluster_name): await message.answer( text="❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя.", - reply_markup=build_admin_back_kb("servers"), + reply_markup=build_admin_back_kb("clusters"), ) return @@ -131,25 +133,25 @@ async def handle_server_name_input(message: Message, state: FSMContext, session: text = ( f"Введите API URL для сервера {server_name} в кластере {cluster_name}:\n\n" - "API URL должен быть в следующем формате:\n\n" - "https://your_domain:port/panel_path\n\n" - "URL должен быть без слэша на конце!\n" + "Ссылку можно найти в поисковой строке браузера, при входе в 3X-UI.\n\n" + "ℹ️ Формат API URL:\n" + "https://your_domain:port/panel_path/" ) await message.answer( text=text, - reply_markup=build_admin_back_kb("servers"), + reply_markup=build_admin_back_kb("clusters"), ) - await state.set_state(AdminServersEditor.waiting_for_api_url) + await state.set_state(AdminClusterStates.waiting_for_api_url) -@router.message(AdminServersEditor.waiting_for_api_url, IsAdminFilter()) -async def handle_api_url_input(message: Message, state: FSMContext, session: Any): +@router.message(AdminClusterStates.waiting_for_api_url, IsAdminFilter()) +async def handle_api_url_input(message: 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("clusters"), ) return @@ -162,26 +164,25 @@ async def handle_api_url_input(message: Message, state: FSMContext, session: Any text = ( f"Введите subscription_url для сервера {server_name} в кластере {cluster_name}:\n\n" - "Subscription URL должен быть в следующем формате:\n\n" - "https://your_domain:port_sub/sub_path\n\n" - "URL должен быть без слэша и имени клиента на конце!\n" - "Его можно увидеть в панели 3x-ui в информации о клиенте." + "Ссылку можно найти в панели 3X-UI, в информации о клиенте.\n\n" + "ℹ️ Формат Subscription URL:\n" + "https://your_domain:port_sub/sub_path/" ) await message.answer( text=text, - reply_markup=build_admin_back_kb("servers"), + reply_markup=build_admin_back_kb("clusters"), ) - await state.set_state(AdminServersEditor.waiting_for_subscription_url) + await state.set_state(AdminClusterStates.waiting_for_subscription_url) -@router.message(AdminServersEditor.waiting_for_subscription_url, IsAdminFilter()) +@router.message(AdminClusterStates.waiting_for_subscription_url, IsAdminFilter()) async def handle_subscription_url_input(message: 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("clusters"), ) return @@ -199,19 +200,19 @@ async def handle_subscription_url_input(message: Message, state: FSMContext): await message.answer( text=text, - reply_markup=build_admin_back_kb("servers"), + reply_markup=build_admin_back_kb("clusters"), ) - await state.set_state(AdminServersEditor.waiting_for_inbound_id) + await state.set_state(AdminClusterStates.waiting_for_inbound_id) -@router.message(AdminServersEditor.waiting_for_inbound_id, IsAdminFilter()) +@router.message(AdminClusterStates.waiting_for_inbound_id, IsAdminFilter()) async def handle_inbound_id_input(message: 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("clusters"), ) return @@ -237,15 +238,15 @@ async def handle_inbound_id_input(message: Message, state: FSMContext): await message.answer( text=f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!", - reply_markup=build_admin_back_kb("servers"), + reply_markup=build_admin_back_kb("clusters"), ) await state.clear() -@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_manage"), IsAdminFilter()) +@router.callback_query(AdminClusterCallback.filter(F.action == "manage"), IsAdminFilter()) async def handle_clusters_manage( - callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any ): cluster_name = callback_data.data @@ -258,9 +259,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, session: Any +@router.callback_query(AdminClusterCallback.filter(F.action == "availability"), IsAdminFilter()) +async def handle_cluster_availability( + callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any ): cluster_name = callback_data.data @@ -278,8 +279,8 @@ async def handle_servers_availability( await callback_query.message.edit_text(text=text) - result_text = f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n" total_online_users = 0 + result_text = f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n" for server in cluster_servers: xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD, logger=logger) @@ -289,92 +290,17 @@ async def handle_servers_availability( online_users = len(await xui.client.online()) total_online_users += online_users result_text += f"🌍 {server['server_name']}: {online_users} активных пользователей.\n" - except Exception as e: result_text += f"❌ {server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n" result_text += f"\n👥 Общее количество активных пользователей в кластере: {total_online_users}." - await callback_query.message.edit_text(text=result_text, reply_markup=build_admin_back_kb("servers")) + await callback_query.message.edit_text(text=result_text, reply_markup=build_admin_back_kb("clusters")) -@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_manage"), IsAdminFilter()) -async def handle_servers_manage(callback_query: CallbackQuery, callback_data: AdminServerEditorCallback): - server_name = callback_data.data - 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) - ) - - if server: - api_url = server["api_url"] - subscription_url = server["subscription_url"] - inbound_id = server["inbound_id"] - - text = ( - f"🔧 Информация о сервере {server_name}:\n\n" - f"📡 API URL: {api_url}\n" - f"🌐 Subscription URL: {subscription_url}\n" - f"🔑 Inbound ID: {inbound_id}" - ) - - await callback_query.message.edit_text( - text=text, - reply_markup=build_manage_server_kb(server_name, cluster_name), - ) - else: - await callback_query.message.edit_text(text="❌ Сервер не найден.") - - -@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_delete"), IsAdminFilter()) -async def handle_servers_delete(callback_query: CallbackQuery, callback_data: AdminServerEditorCallback): - server_name = callback_data.data - - await callback_query.message.edit_text( - text=f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?", - reply_markup=build_delete_server_kb(server_name), - ) - - -@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_delete_confirm"), IsAdminFilter()) -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) - - await callback_query.message.edit_text( - text=f"🗑️ Сервер {server_name} успешно удален.", reply_markup=build_admin_back_kb("servers") - ) - - -@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_add"), IsAdminFilter()) -async def handle_servers_add( - callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, state: FSMContext -): - cluster_name = callback_data.data - - await state.update_data(cluster_name=cluster_name) - - text = ( - f"Введите имя сервера для кластера {cluster_name}:\n\n" - "Рекомендуется указать локацию сервера в имени.\n\n" - "Пример: server-asia, server-europe" - ) - - await callback_query.message.edit_text( - text=text, - reply_markup=build_admin_back_kb("servers"), - ) - - await state.set_state(AdminServersEditor.waiting_for_server_name) - - -@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_backup"), IsAdminFilter()) +@router.callback_query(AdminClusterCallback.filter(F.action == "backup"), IsAdminFilter()) async def handle_clusters_backup( - callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any ): cluster_name = callback_data.data @@ -397,14 +323,12 @@ async def handle_clusters_backup( await callback_query.message.edit_text( text=text, - reply_markup=build_admin_back_kb("servers"), + reply_markup=build_admin_back_kb("clusters"), ) -@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_sync"), IsAdminFilter()) -async def handle_clusters_sync( - callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any -): +@router.callback_query(AdminClusterCallback.filter(F.action == "sync"), IsAdminFilter()) +async def handle_clusters_sync(callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any): cluster_name = callback_data.data try: @@ -418,7 +342,7 @@ async def handle_clusters_sync( if not keys_to_sync: await callback_query.message.answer( text=f"❌ Нет ключей для синхронизации в кластере {cluster_name}.", - reply_markup=build_admin_back_kb("servers"), + reply_markup=build_admin_back_kb("clusters"), ) return @@ -437,59 +361,10 @@ async def handle_clusters_sync( await callback_query.message.answer( text=f"✅ Ключи успешно синхронизированы для кластера {cluster_name}", - reply_markup=build_admin_back_kb("servers"), + reply_markup=build_admin_back_kb("clusters"), ) except Exception as e: logger.error(f"Ошибка синхронизации ключей в кластере {cluster_name}: {e}") await callback_query.message.answer( - text=f"❌ Произошла ошибка при синхронизации: {e}", reply_markup=build_admin_back_kb("servers") + text=f"❌ Произошла ошибка при синхронизации: {e}", reply_markup=build_admin_back_kb("clusters") ) - - -@router.callback_query(AdminPanelCallback.filter(F.action == "change_domain"), IsAdminFilter()) -async def request_new_domain(callback_query: CallbackQuery, state: FSMContext): - """Запрашивает у администратора новый домен.""" - await state.set_state(AdminServersEditor.waiting_for_new_domain) - await callback_query.message.edit_text( - text="🌐 Введите новый домен (без https://):\nПример: pocomachodomen.ru", - ) - - -@router.message(AdminServersEditor.waiting_for_new_domain) -async def process_new_domain(message: Message, state: FSMContext, session: asyncpg.Connection): - """Обновляет домен в таблице keys.""" - new_domain = message.text.strip() - logger.info(f"[DomainChange] Новый домен, введённый администратором: '{new_domain}'") - - if not new_domain or " " in new_domain or not new_domain.replace(".", "").isalnum(): - logger.warning("[DomainChange] Некорректный домен") - await message.answer( - "🚫 Некорректный домен! Введите домен без http:// и без пробелов.", - reply_markup=build_admin_back_kb("admin"), - ) - return - - new_domain_url = f"https://{new_domain}" - logger.info(f"[DomainChange] Новый домен с протоколом: '{new_domain_url}'") - - query = """ - UPDATE keys - SET key = regexp_replace(key, '^https://[^/]+', $1::TEXT) - WHERE key NOT LIKE $1 || '%' - """ - try: - await session.execute(query, new_domain_url) - logger.info("[DomainChange] Запрос на обновление домена выполнен успешно.") - except Exception as e: - logger.error(f"[DomainChange] Ошибка при выполнении запроса: {e}") - await message.answer(f"❌ Ошибка при обновлении домена: {e}", reply_markup=build_admin_back_kb("admin")) - return - - try: - sample = await session.fetchrow("SELECT key FROM keys LIMIT 1") - logger.info(f"[DomainChange] Пример обновленной записи: {sample}") - except Exception as e: - logger.error(f"[DomainChange] Ошибка при выборке обновленной записи: {e}") - - await message.answer(f"✅ Домен успешно изменен на {new_domain}!", reply_markup=build_admin_back_kb("admin")) - await state.clear() diff --git a/handlers/admin/clusters/keyboard.py b/handlers/admin/clusters/keyboard.py new file mode 100644 index 00000000..bb3d61b2 --- /dev/null +++ b/handlers/admin/clusters/keyboard.py @@ -0,0 +1,63 @@ +from aiogram.filters.callback_data import CallbackData +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from ..panel.keyboard import build_admin_back_btn +from ..servers.keyboard import AdminServerCallback + + +class AdminClusterCallback(CallbackData, prefix="admin_cluster"): + action: str + data: str + + +def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + cluster_names = list(servers.keys()) + for i in range(0, len(cluster_names), 2): + row_buttons = [] + for cluster_name in cluster_names[i:i+2]: + row_buttons.append( + InlineKeyboardButton( + text=f"⚙️ {cluster_name}", + callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack(), + ) + ) + builder.row(*row_buttons) + + builder.button(text="➕ Добавить кластер", callback_data=AdminClusterCallback(action="add").pack()) + builder.row(build_admin_back_btn()) + return builder.as_markup() + + +def build_manage_cluster_kb(cluster_servers, cluster_name) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + for server in cluster_servers: + builder.button( + text=f"🌍 {server['server_name']}", + callback_data=AdminServerCallback(action="manage", data=server["server_name"]).pack(), + ) + + builder.button( + text="➕ Добавить сервер", + callback_data=AdminServerCallback(action="add", data=cluster_name).pack(), + ) + builder.row( + InlineKeyboardButton( + text="🌐 Доступность", + callback_data=AdminClusterCallback(action="availability", data=cluster_name).pack(), + ), + InlineKeyboardButton( + text="🔄 Синхронизация", + callback_data=AdminClusterCallback(action="sync", data=cluster_name).pack(), + ), + ) + builder.button( + text="💾 Создать бэкап кластера", + callback_data=AdminClusterCallback(action="backup", data=cluster_name).pack(), + ) + builder.row(build_admin_back_btn("servers")) + builder.adjust(1, 1, 1, 1, 1, 2, 1) + return builder.as_markup() diff --git a/handlers/admin/coupons/__init__.py b/handlers/admin/coupons/__init__.py new file mode 100644 index 00000000..1948a0fd --- /dev/null +++ b/handlers/admin/coupons/__init__.py @@ -0,0 +1,3 @@ +__all__ = ("router",) + +from coupons_handler import router diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/coupons/coupons_handler.py similarity index 97% rename from handlers/admin/admin_coupons.py rename to handlers/admin/coupons/coupons_handler.py index b3bf4164..1c7bbe92 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/coupons/coupons_handler.py @@ -8,8 +8,8 @@ from config import USERNAME_BOT from database import create_coupon, delete_coupon, get_all_coupons from filters.admin import IsAdminFilter -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 keyboard import AdminCouponDeleteCallback, build_coupons_kb, build_coupons_list_kb +from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb from logger import logger diff --git a/keyboards/admin/coupons_kb.py b/handlers/admin/coupons/keyboard.py similarity index 95% rename from keyboards/admin/coupons_kb.py rename to handlers/admin/coupons/keyboard.py index 5a21a38d..f65480f1 100644 --- a/keyboards/admin/coupons_kb.py +++ b/handlers/admin/coupons/keyboard.py @@ -2,7 +2,7 @@ from aiogram.filters.callback_data import CallbackData from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder -from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn +from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn class AdminCouponDeleteCallback(CallbackData, prefix="admin_coupon_delete"): diff --git a/handlers/admin/management/__init__.py b/handlers/admin/management/__init__.py new file mode 100644 index 00000000..1a6a84ae --- /dev/null +++ b/handlers/admin/management/__init__.py @@ -0,0 +1,3 @@ +__all__ = ("router",) + +from management_handler import router diff --git a/handlers/admin/management/keyboard.py b/handlers/admin/management/keyboard.py new file mode 100644 index 00000000..61fb3408 --- /dev/null +++ b/handlers/admin/management/keyboard.py @@ -0,0 +1,16 @@ +from aiogram.types import InlineKeyboardMarkup +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from ..panel.keyboard import build_admin_back_btn, AdminPanelCallback + + +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.button(text="🌐 Сменить домен", callback_data=AdminPanelCallback(action="change_domain").pack()) + builder.button(text="🔑 Восстановить пробники", callback_data=AdminPanelCallback(action="restore_trials").pack()) + builder.row(build_admin_back_btn()) + builder.adjust(1) + return builder.as_markup() diff --git a/handlers/admin/management/management_handler.py b/handlers/admin/management/management_handler.py new file mode 100644 index 00000000..cf3629e8 --- /dev/null +++ b/handlers/admin/management/management_handler.py @@ -0,0 +1,73 @@ +from aiogram import F, Router +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import StatesGroup, State +from aiogram.types import CallbackQuery, Message +from asyncpg import Connection + +from filters.admin import IsAdminFilter +from keyboard import AdminPanelCallback, build_management_kb +from logger import logger +from ..panel.keyboard import build_admin_back_kb + +router = Router() + + +class AdminManagementStates(StatesGroup): + waiting_for_new_domain = State() + + +@router.callback_query(AdminPanelCallback.filter(F.action == "management"), IsAdminFilter()) +async def handle_management(callback_query: CallbackQuery): + await callback_query.message.edit_text( + text="🤖 Управление ботом", + reply_markup=build_management_kb(), + ) + + +@router.callback_query(AdminPanelCallback.filter(F.action == "change_domain"), IsAdminFilter()) +async def request_new_domain(callback_query: AdminPanelCallback, state: FSMContext): + """Запрашивает у администратора новый домен.""" + await state.set_state(AdminManagementStates.waiting_for_new_domain) + await callback_query.message.edit_text( + text="🌐 Введите новый домен (без https://):\nПример: pocomachodomen.ru", + ) + + +@router.message(AdminManagementStates.waiting_for_new_domain) +async def process_new_domain(message: Message, state: FSMContext, session: Connection): + """Обновляет домен в таблице keys.""" + new_domain = message.text.strip() + logger.info(f"[DomainChange] Новый домен, введённый администратором: '{new_domain}'") + + if not new_domain or " " in new_domain or not new_domain.replace(".", "").isalnum(): + logger.warning("[DomainChange] Некорректный домен") + await message.answer( + "🚫 Некорректный домен! Введите домен без http:// и без пробелов.", + reply_markup=build_admin_back_kb("admin"), + ) + return + + new_domain_url = f"https://{new_domain}" + logger.info(f"[DomainChange] Новый домен с протоколом: '{new_domain_url}'") + + query = """ + UPDATE keys + SET key = regexp_replace(key, '^https://[^/]+', $1::TEXT) + WHERE key NOT LIKE $1 || '%' + """ + try: + await session.execute(query, new_domain_url) + logger.info("[DomainChange] Запрос на обновление домена выполнен успешно.") + except Exception as e: + logger.error(f"[DomainChange] Ошибка при выполнении запроса: {e}") + await message.answer(f"❌ Ошибка при обновлении домена: {e}", reply_markup=build_admin_back_kb("admin")) + return + + try: + sample = await session.fetchrow("SELECT key FROM keys LIMIT 1") + logger.info(f"[DomainChange] Пример обновленной записи: {sample}") + except Exception as e: + logger.error(f"[DomainChange] Ошибка при выборке обновленной записи: {e}") + + await message.answer(f"✅ Домен успешно изменен на {new_domain}!", reply_markup=build_admin_back_kb("admin")) + await state.clear() diff --git a/handlers/admin/panel/__init__.py b/handlers/admin/panel/__init__.py new file mode 100644 index 00000000..05a477e6 --- /dev/null +++ b/handlers/admin/panel/__init__.py @@ -0,0 +1,3 @@ +__all__ = ("router",) + +from panel_handler import router diff --git a/keyboards/admin/panel_kb.py b/handlers/admin/panel/keyboard.py similarity index 69% rename from keyboards/admin/panel_kb.py rename to handlers/admin/panel/keyboard.py index 1c53a8ae..4fab750c 100644 --- a/keyboards/admin/panel_kb.py +++ b/handlers/admin/panel/keyboard.py @@ -1,3 +1,5 @@ +from typing import Any + from aiogram.filters.callback_data import CallbackData from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder @@ -7,10 +9,10 @@ class AdminPanelCallback(CallbackData, prefix="admin_panel"): action: str page: int - def __init__(self, *args, **kwargs) -> None: - if "page" not in kwargs or kwargs["page"] is None: - kwargs["page"] = 1 - super().__init__(*args, **kwargs) + def __init__(self, /, **data: Any) -> None: + if "page" not in data or data["page"] is None: + data["page"] = 1 + super().__init__(**data) def build_panel_kb() -> InlineKeyboardMarkup: @@ -31,18 +33,6 @@ def build_panel_kb() -> InlineKeyboardMarkup: 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.button(text="🌐 Сменить домен", callback_data=AdminPanelCallback(action="change_domain").pack()) - builder.button(text="🔑 Восстановить пробники", callback_data=AdminPanelCallback(action="restore_trials").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()) diff --git a/handlers/admin/admin_panel.py b/handlers/admin/panel/panel_handler.py similarity index 78% rename from handlers/admin/admin_panel.py rename to handlers/admin/panel/panel_handler.py index d940c2c7..328eb047 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/panel/panel_handler.py @@ -5,8 +5,7 @@ from aiogram.types import CallbackQuery, Message from bot import version from filters.admin import IsAdminFilter -from keyboards.admin.panel_kb import AdminPanelCallback, build_management_kb, build_panel_kb - +from keyboard import AdminPanelCallback, build_panel_kb router = Router() @@ -39,11 +38,3 @@ async def handle_admin_message(message: Message, state: FSMContext): await state.clear() await message.answer(text=text, reply_markup=build_panel_kb()) - - -@router.callback_query(AdminPanelCallback.filter(F.action == "management"), IsAdminFilter()) -async def handle_management(callback_query: CallbackQuery): - await callback_query.message.edit_text( - text="🤖 Управление ботом", - reply_markup=build_management_kb(), - ) diff --git a/handlers/admin/restart/__init__.py b/handlers/admin/restart/__init__.py new file mode 100644 index 00000000..7ac5de76 --- /dev/null +++ b/handlers/admin/restart/__init__.py @@ -0,0 +1,3 @@ +__all__ = ("router",) + +from restart_handler import router diff --git a/handlers/admin/admin_restart.py b/handlers/admin/restart/restart_handler.py similarity index 93% rename from handlers/admin/admin_restart.py rename to handlers/admin/restart/restart_handler.py index 9acc1195..12b4c013 100644 --- a/handlers/admin/admin_restart.py +++ b/handlers/admin/restart/restart_handler.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 AdminPanelCallback, build_admin_back_kb, build_restart_kb +from handlers.admin.panel.keyboard import AdminPanelCallback, build_admin_back_kb, build_restart_kb router = Router() diff --git a/handlers/admin/sender/__init__.py b/handlers/admin/sender/__init__.py new file mode 100644 index 00000000..169939bc --- /dev/null +++ b/handlers/admin/sender/__init__.py @@ -0,0 +1,3 @@ +__all__ = ("router",) + +from sender_handler import router diff --git a/keyboards/admin/sender_kb.py b/handlers/admin/sender/keyboard.py similarity index 95% rename from keyboards/admin/sender_kb.py rename to handlers/admin/sender/keyboard.py index 82c37115..0a4991eb 100644 --- a/keyboards/admin/sender_kb.py +++ b/handlers/admin/sender/keyboard.py @@ -2,7 +2,7 @@ from aiogram.filters.callback_data import CallbackData from aiogram.types import InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder -from keyboards.admin.panel_kb import build_admin_back_btn +from ..panel.keyboard import build_admin_back_btn class AdminSenderCallback(CallbackData, prefix="admin_sender"): diff --git a/handlers/admin/admin_sender.py b/handlers/admin/sender/sender_handler.py similarity index 96% rename from handlers/admin/admin_sender.py rename to handlers/admin/sender/sender_handler.py index e85338aa..bfe33492 100644 --- a/handlers/admin/admin_sender.py +++ b/handlers/admin/sender/sender_handler.py @@ -7,8 +7,8 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, Message from filters.admin import IsAdminFilter -from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb -from keyboards.admin.sender_kb import AdminSenderCallback, build_clusters_kb, build_sender_kb +from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb +from keyboard import AdminSenderCallback, build_clusters_kb, build_sender_kb from logger import logger diff --git a/handlers/admin/servers/__init__.py b/handlers/admin/servers/__init__.py new file mode 100644 index 00000000..c4319df5 --- /dev/null +++ b/handlers/admin/servers/__init__.py @@ -0,0 +1,3 @@ +__all__ = ("router",) + +from servers_handler import router diff --git a/handlers/admin/servers/keyboard.py b/handlers/admin/servers/keyboard.py new file mode 100644 index 00000000..9077ff4d --- /dev/null +++ b/handlers/admin/servers/keyboard.py @@ -0,0 +1,26 @@ +from aiogram.filters.callback_data import CallbackData +from aiogram.types import InlineKeyboardMarkup +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from ..clusters.keyboard import AdminClusterCallback + + +class AdminServerCallback(CallbackData, prefix="admin_server"): + action: str + data: str + + +def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.button(text="🗑️ Удалить", callback_data=AdminServerCallback(action="delete", data=server_name).pack()) + builder.button(text="🔙 Назад", callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack()) + builder.adjust(1) + return builder.as_markup() + + +def build_delete_server_kb(server_name: str) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.button(text="✅ Да", callback_data=AdminServerCallback(action="delete_confirm", data=server_name).pack()) + builder.button(text="🔙 Назад", callback_data=AdminServerCallback(action="manage", data=server_name).pack()) + builder.adjust(1) + return builder.as_markup() diff --git a/handlers/admin/servers/servers_handler.py b/handlers/admin/servers/servers_handler.py new file mode 100644 index 00000000..c5199bd6 --- /dev/null +++ b/handlers/admin/servers/servers_handler.py @@ -0,0 +1,67 @@ +from typing import Any + +from aiogram import F, Router, types +from aiogram.types import CallbackQuery + +from database import delete_server, get_servers +from filters.admin import IsAdminFilter +from keyboard import ( + AdminServerCallback, + build_delete_server_kb, + build_manage_server_kb, +) +from ..panel.keyboard import build_admin_back_kb + +router = Router() + + +@router.callback_query(AdminServerCallback.filter(F.action == "manage"), IsAdminFilter()) +async def handle_server_manage(callback_query: CallbackQuery, callback_data: AdminServerCallback): + server_name = callback_data.data + 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) + ) + + if server: + api_url = server["api_url"] + subscription_url = server["subscription_url"] + inbound_id = server["inbound_id"] + + text = ( + f"🔧 Информация о сервере {server_name}:\n\n" + f"📡 API URL: {api_url}\n" + f"🌐 Subscription URL: {subscription_url}\n" + f"🔑 Inbound ID: {inbound_id}" + ) + + await callback_query.message.edit_text( + text=text, + reply_markup=build_manage_server_kb(server_name, cluster_name), + ) + else: + await callback_query.message.edit_text(text="❌ Сервер не найден.") + + +@router.callback_query(AdminServerCallback.filter(F.action == "delete"), IsAdminFilter()) +async def handle_server_delete(callback_query: CallbackQuery, callback_data: AdminServerCallback): + server_name = callback_data.data + + await callback_query.message.edit_text( + text=f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?", + reply_markup=build_delete_server_kb(server_name), + ) + + +@router.callback_query(AdminServerCallback.filter(F.action == "delete_confirm"), IsAdminFilter()) +async def handle_server_delete_confirm( + callback_query: types.CallbackQuery, callback_data: AdminServerCallback, session: Any +): + 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("clusters") + ) diff --git a/handlers/admin/stats/__init__.py b/handlers/admin/stats/__init__.py new file mode 100644 index 00000000..cd21d017 --- /dev/null +++ b/handlers/admin/stats/__init__.py @@ -0,0 +1,3 @@ +__all__ = ("router",) + +from stats_handler import router diff --git a/keyboards/admin/stats_kb.py b/handlers/admin/stats/keyboard.py similarity index 90% rename from keyboards/admin/stats_kb.py rename to handlers/admin/stats/keyboard.py index b8046593..cc0c3167 100644 --- a/keyboards/admin/stats_kb.py +++ b/handlers/admin/stats/keyboard.py @@ -1,7 +1,7 @@ from aiogram.types import InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder -from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn +from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn def build_stats_kb() -> InlineKeyboardMarkup: diff --git a/handlers/admin/admin_stats.py b/handlers/admin/stats/stats_handler.py similarity index 97% rename from handlers/admin/admin_stats.py rename to handlers/admin/stats/stats_handler.py index 042036c7..92e68c33 100644 --- a/handlers/admin/admin_stats.py +++ b/handlers/admin/stats/stats_handler.py @@ -8,8 +8,8 @@ from aiogram.exceptions import TelegramBadRequest from aiogram.types import CallbackQuery from filters.admin import IsAdminFilter -from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb -from keyboards.admin.stats_kb import build_stats_kb +from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb +from keyboard import build_stats_kb from logger import logger from utils.csv_export import export_payments_csv, export_users_csv diff --git a/handlers/admin/users/__init__.py b/handlers/admin/users/__init__.py new file mode 100644 index 00000000..139f10e5 --- /dev/null +++ b/handlers/admin/users/__init__.py @@ -0,0 +1,3 @@ +__all__ = ("router",) + +from users_handler import router diff --git a/keyboards/admin/users_kb.py b/handlers/admin/users/keyboard.py similarity index 99% rename from keyboards/admin/users_kb.py rename to handlers/admin/users/keyboard.py index 72b24ad3..9697c526 100644 --- a/keyboards/admin/users_kb.py +++ b/handlers/admin/users/keyboard.py @@ -5,7 +5,7 @@ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from config import RENEWAL_PRICES -from keyboards.admin.panel_kb import build_admin_back_btn +from ..panel.keyboard import build_admin_back_btn class AdminUserEditorCallback(CallbackData, prefix="admin_users"): diff --git a/handlers/admin/admin_users.py b/handlers/admin/users/users_handler.py similarity index 98% rename from handlers/admin/admin_users.py rename to handlers/admin/users/users_handler.py index 9e658fb9..db48c12e 100644 --- a/handlers/admin/admin_users.py +++ b/handlers/admin/users/users_handler.py @@ -27,7 +27,7 @@ from database import ( from filters.admin import IsAdminFilter from handlers.keys.key_utils import delete_key_from_cluster, get_user_traffic, renew_key_in_cluster, update_subscription from handlers.utils import sanitize_key_name -from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb, build_admin_back_btn +from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb, build_admin_back_btn from keyboards.admin.users_kb import ( AdminUserEditorCallback, AdminUserKeyEditorCallback, @@ -733,10 +733,11 @@ async def confirm_restore_trials(callback_query: types.CallbackQuery): await callback_query.message.edit_text( text="⚠ Вы уверены, что хотите восстановить пробники для пользователей? \n\n" - "Только для тех, у кого нет активной подписки!", - reply_markup=builder.as_markup() + "Только для тех, у кого нет активной подписки!", + reply_markup=builder.as_markup(), ) + @router.callback_query(AdminPanelCallback.filter(F.action == "confirm_restore_trials"), IsAdminFilter()) async def restore_trials(callback_query: types.CallbackQuery, session: Any): """ @@ -759,15 +760,13 @@ async def restore_trials(callback_query: types.CallbackQuery, session: Any): await callback_query.message.edit_text( text="✅ Пробники успешно восстановлены для пользователей, у которых нет активных подписок.", - reply_markup=builder.as_markup() + reply_markup=builder.as_markup(), ) @router.callback_query(AdminUserEditorCallback.filter(F.action == "users_export_referrals"), IsAdminFilter()) async def handle_users_export_referrals( - callback_query: types.CallbackQuery, - callback_data: AdminUserEditorCallback, - session: Any + callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any ): """ Обработчик: получает tg_id реферера из callback_data, @@ -783,6 +782,5 @@ async def handle_users_export_referrals( return await callback_query.message.answer_document( - document=csv_file, - caption=f"Список рефералов для пользователя {referrer_tg_id}." - ) \ No newline at end of file + document=csv_file, caption=f"Список рефералов для пользователя {referrer_tg_id}." + ) diff --git a/handlers/buttons/yookassa.py b/handlers/buttons/yookassa.py index 387fc745..5aaded81 100644 --- a/handlers/buttons/yookassa.py +++ b/handlers/buttons/yookassa.py @@ -4,4 +4,4 @@ BACK = "⬅️ Назад" CUSTOM_SUM = "💰 Ввести свою сумму" CUSTOM_SUM_ANSWER = "Пожалуйста, введите сумму пополнения." PROFILE = "👤 Личный кабинет" -DEFAULT_PAYMENT_MESSAGE = "Вы выбрали пополнение на {amount} рублей. Перейдите по ссылке для оплаты:" \ No newline at end of file +DEFAULT_PAYMENT_MESSAGE = "Вы выбрали пополнение на {amount} рублей. Перейдите по ссылке для оплаты:" diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 2f6d2b15..28eee02a 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -26,7 +26,7 @@ from config import ( TOTAL_GB, USE_COUNTRY_SELECTION, USE_NEW_PAYMENT_FLOW, - TOGGLE_CLIENT + TOGGLE_CLIENT, ) from bot import bot @@ -177,7 +177,7 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any) reply_markup=keyboard, media_path=image_path, ) - + else: key = record["key"] expiry_time = record["expiry_time"] @@ -188,17 +188,13 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any) time_left = expiry_date - current_date if time_left.total_seconds() <= 0: - days_left_message = ( - "🕒 Статус подписки:\n🔴 Истекла\nОсталось часов: 0\nОсталось минут: 0" - ) + days_left_message = "🕒 Статус подписки:\n🔴 Истекла\nОсталось часов: 0\nОсталось минут: 0" else: total_seconds = int(time_left.total_seconds()) days = total_seconds // 86400 hours = (total_seconds % 86400) // 3600 minutes = (total_seconds % 3600) // 60 - days_left_message = ( - f"Осталось: {days} дней, {hours} часов, {minutes} минут" - ) + days_left_message = f"Осталось: {days} дней, {hours} часов, {minutes} минут" formatted_expiry_date = expiry_date.strftime("%d %B %Y года") response_message = key_message( @@ -248,18 +244,12 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any) ) else: builder.row( - InlineKeyboardButton( - text="⏳ Продлить подписку", - callback_data=f"renew_key|{key_name}" - ) + InlineKeyboardButton(text="⏳ Продлить подписку", callback_data=f"renew_key|{key_name}") ) if USE_COUNTRY_SELECTION: builder.row( - InlineKeyboardButton( - text="🌍 Сменить локацию", - callback_data=f"change_location|{key_name}" - ) + InlineKeyboardButton(text="🌍 Сменить локацию", callback_data=f"change_location|{key_name}") ) if TOGGLE_CLIENT: @@ -300,6 +290,7 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any) async def process_callback_unfreeze_subscription(callback_query: CallbackQuery, session: Any): key_name = callback_query.data.split("|")[1] confirm_text = UNFREEZE_SUBSCRIPTION_CONFIRM_MSG + builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton( @@ -355,7 +346,7 @@ async def process_callback_unfreeze_subscription_confirm(callback_query: Callbac """, new_expiry_time, record["tg_id"], - client_id + client_id, ) await renew_key_in_cluster( @@ -367,9 +358,7 @@ async def process_callback_unfreeze_subscription_confirm(callback_query: Callbac ) text_ok = SUBSCRIPTION_UNFROZEN_MSG builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}") - ) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}")) await edit_or_send_message( target_message=callback_query.message, text=text_ok, @@ -377,13 +366,10 @@ async def process_callback_unfreeze_subscription_confirm(callback_query: Callbac ) else: text_error = ( - "Произошла ошибка при включении подписки.\n" - f"Детали: {result.get('error') or result.get('results')}" + f"Произошла ошибка при включении подписки.\nДетали: {result.get('error') or result.get('results')}" ) builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}") - ) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}")) await edit_or_send_message( target_message=callback_query.message, text=text_error, @@ -447,7 +433,7 @@ async def process_callback_freeze_subscription_confirm(callback_query: CallbackQ now_ms = int(time.time() * 1000) time_left = record["expiry_time"] - now_ms if time_left < 0: - time_left = 0 + time_left = 0 update_result = await session.execute( """ @@ -459,14 +445,12 @@ async def process_callback_freeze_subscription_confirm(callback_query: CallbackQ """, time_left, record["tg_id"], - client_id + client_id, ) text_ok = SUBSCRIPTION_FROZEN_MSG builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}") - ) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}")) await edit_or_send_message( target_message=callback_query.message, text=text_ok, @@ -474,13 +458,10 @@ async def process_callback_freeze_subscription_confirm(callback_query: CallbackQ ) else: text_error = ( - "Произошла ошибка при заморозке подписки.\n" - f"Детали: {result.get('error') or result.get('results')}" + f"Произошла ошибка при заморозке подписки.\nДетали: {result.get('error') or result.get('results')}" ) builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}") - ) + builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}")) await edit_or_send_message( target_message=callback_query.message, text=text_error, diff --git a/handlers/notifications/general_notifications.py b/handlers/notifications/general_notifications.py index 2cfaa6f3..d11f6695 100644 --- a/handlers/notifications/general_notifications.py +++ b/handlers/notifications/general_notifications.py @@ -15,7 +15,7 @@ from config import ( RENEWAL_PRICES, TOTAL_GB, TRIAL_TIME_DISABLE, - NOTIFY_INACTIVE_TRAFFIC + NOTIFY_INACTIVE_TRAFFIC, ) from database import ( add_notification, diff --git a/handlers/profile.py b/handlers/profile.py index f11c410e..043d3e52 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -40,14 +40,6 @@ from handlers.buttons.profile import ( MY_SUBS, PAYMENT, ) -from handlers.texts import ( - get_referral_link, - invite_message_send, - profile_message_send, - BALANCE_MANAGEMENT_TEXT, - BALANCE_HISTORY_HEADER, - INVITE_TEXT_NON_INLINE -) from keyboards.admin.panel_kb import AdminPanelCallback from logger import logger diff --git a/keyboards/admin/__init__.py b/keyboards/admin/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/keyboards/admin/servers_kb.py b/keyboards/admin/servers_kb.py deleted file mode 100644 index e7d148e8..00000000 --- a/keyboards/admin/servers_kb.py +++ /dev/null @@ -1,81 +0,0 @@ -from aiogram.filters.callback_data import CallbackData -from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup -from aiogram.utils.keyboard import InlineKeyboardBuilder - -from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_btn - - -class AdminServerEditorCallback(CallbackData, prefix="admin_servers"): - action: str - data: str - - -def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup: - builder = InlineKeyboardBuilder() - - for cluster_name in servers: - builder.button( - text=f"⚙️ {cluster_name}", - 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.adjust(1) - return builder.as_markup() - - -def build_manage_cluster_kb(cluster_servers, cluster_name) -> InlineKeyboardMarkup: - builder = InlineKeyboardBuilder() - - for server in cluster_servers: - builder.button( - text=f"🌍 {server['server_name']}", - callback_data=AdminServerEditorCallback(action="servers_manage", data=server["server_name"]).pack(), - ) - - builder.button( - text="➕ Добавить сервер", - callback_data=AdminServerEditorCallback(action="servers_add", data=cluster_name).pack(), - ) - builder.row( - InlineKeyboardButton( - text="🌐 Доступность", - callback_data=AdminServerEditorCallback(action="servers_availability", data=cluster_name).pack(), - ), - InlineKeyboardButton( - text="🔄 Синхронизация", - callback_data=AdminServerEditorCallback(action="clusters_sync", data=cluster_name).pack(), - ), - ) - builder.button( - text="💾 Создать бэкап кластера", - callback_data=AdminServerEditorCallback(action="clusters_backup", data=cluster_name).pack(), - ) - builder.row(build_admin_back_btn("servers")) - builder.adjust(1, 1, 1, 1, 1, 2, 1) - return builder.as_markup() - - -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() - ) - builder.button( - text="🔙 Назад", callback_data=AdminServerEditorCallback(action="clusters_manage", data=cluster_name).pack() - ) - builder.adjust(1) - return builder.as_markup() - - -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() - ) - builder.button( - text="🔙 Назад", callback_data=AdminServerEditorCallback(action="servers_manage", data=server_name).pack() - ) - builder.adjust(1) - return builder.as_markup() diff --git a/middlewares/loggings.py b/middlewares/loggings.py index db4a2ebf..f329759f 100644 --- a/middlewares/loggings.py +++ b/middlewares/loggings.py @@ -1,5 +1,5 @@ from collections.abc import Awaitable, Callable -from typing import Any,TypedDict +from typing import Any, TypedDict from aiogram import BaseMiddleware from aiogram.types import CallbackQuery, InlineQuery, Message, TelegramObject, User diff --git a/middlewares/throttling.py b/middlewares/throttling.py index 50e20bf4..710744c5 100644 --- a/middlewares/throttling.py +++ b/middlewares/throttling.py @@ -28,4 +28,4 @@ class ThrottlingMiddleware(BaseMiddleware): else: self.cache[user_id] = current_count + 1 - return await handler(event, data) \ No newline at end of file + return await handler(event, data) diff --git a/servers.py b/servers.py index d23bda94..b403e24f 100644 --- a/servers.py +++ b/servers.py @@ -60,10 +60,7 @@ async def notify_admin(server_name: str, status: str, down_duration: timedelta = ) else: downtime = str(down_duration).split(".")[0] - message = ( - f"✅ Сервер '{server_name}' снова в сети!\n\n" - f"⏳ Время простоя: {downtime}." - ) + message = f"✅ Сервер '{server_name}' снова в сети!\n\n⏳ Время простоя: {downtime}." for admin_id in ADMIN_ID: logger.info(f"📨 Отправляем уведомление '{status}' администратору {admin_id} о сервере {server_name}") @@ -119,11 +116,13 @@ async def check_servers(): if last_ping_time is None: last_ping_times[server_name] = current_time - last_down_times[server_name] = current_time + last_down_times[server_name] = current_time if last_ping_time and (current_time - last_ping_time > timedelta(seconds=PING_TIME * 3)): if server_name not in notified_servers: - logger.warning(f"🚨 Уведомление: сервер {server_name} не отвечает более {PING_TIME * 3} секунд!") + logger.warning( + f"🚨 Уведомление: сервер {server_name} не отвечает более {PING_TIME * 3} секунд!" + ) await notify_admin(server_name, "down") notified_servers.add(server_name) last_down_times[server_name] = current_time @@ -145,4 +144,4 @@ async def check_servers(): def extract_host(api_url: str) -> str: """Извлекает хост из `api_url`.""" match = re.match(r"(https?://)?([^:/]+)", api_url) - return match.group(2) if match else api_url \ No newline at end of file + return match.group(2) if match else api_url diff --git a/utils/csv_export.py b/utils/csv_export.py index 276c79ea..179249c7 100644 --- a/utils/csv_export.py +++ b/utils/csv_export.py @@ -115,14 +115,14 @@ async def export_referrals_csv(referrer_tg_id: int, session: Any) -> BufferedInp WHERE r.referrer_tg_id = $1 ORDER BY r.referred_tg_id """, - referrer_tg_id + referrer_tg_id, ) if not rows: return None output = StringIO() - writer = csv.writer(output, delimiter=';') + writer = csv.writer(output, delimiter=";") writer.writerow(["Приглашённый (tg_id)", "Имя"]) for row in rows: @@ -136,4 +136,4 @@ async def export_referrals_csv(referrer_tg_id: int, session: Any) -> BufferedInp csv_data = output.getvalue().encode("utf-8") filename = f"referrals_{referrer_tg_id}.csv" - return BufferedInputFile(file=csv_data, filename=filename) \ No newline at end of file + return BufferedInputFile(file=csv_data, filename=filename)