diff --git a/backup.py b/backup.py index e9737a7e..d2b2bb11 100644 --- a/backup.py +++ b/backup.py @@ -1,15 +1,13 @@ import os import subprocess - from datetime import datetime, timedelta from pathlib import Path import aiofiles - from aiogram.types import BufferedInputFile -from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT from bot import bot +from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT from logger import logger diff --git a/bot.py b/bot.py index ba5b5b0b..e43debf1 100644 --- a/bot.py +++ b/bot.py @@ -8,13 +8,12 @@ from aiogram.filters import ExceptionTypeFilter from aiogram.fsm.storage.memory import MemoryStorage from aiogram.types import BufferedInputFile, ErrorEvent from aiogram.utils.markdown import hbold -from config import ADMIN_ID, API_TOKEN +from config import ADMIN_ID, API_TOKEN from filters.private import IsPrivateFilter from logger import logger from middlewares import register_middleware - bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) storage = MemoryStorage() dp = Dispatcher(bot=bot, storage=storage) diff --git a/client.py b/client.py index c7609ec8..8dbaab1c 100644 --- a/client.py +++ b/client.py @@ -5,7 +5,6 @@ import httpx import py3xui from config import LIMIT_IP, SUPERNODE - from logger import logger diff --git a/database.py b/database.py index 2af0c995..54bcc83d 100644 --- a/database.py +++ b/database.py @@ -1,5 +1,4 @@ import json - from datetime import datetime from typing import Any @@ -7,7 +6,6 @@ import asyncpg import pytz from config import CASHBACK, CHECK_REFERRAL_REWARD_ISSUED, DATABASE_URL, REFERRAL_BONUS_PERCENTAGES - from logger import logger diff --git a/filters/admin.py b/filters/admin.py index d04fa5a3..b6f6e0a2 100644 --- a/filters/admin.py +++ b/filters/admin.py @@ -1,5 +1,6 @@ from aiogram.filters import BaseFilter from aiogram.types import Message + from config import ADMIN_ID diff --git a/handlers/__init__.py b/handlers/__init__.py index bceb1c61..c2cd5125 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -14,7 +14,6 @@ from .payments import router as payments_router from .profile import router as profile_router from .start import router as start_router - router = Router(name="handlers_main_router") router.include_routers( 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..3a2f8d4a --- /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..b466d255 100644 --- a/handlers/admin/admin_backups.py +++ b/handlers/admin/backups/backups_handler.py @@ -3,8 +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..2fc0494c --- /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..360ce4ac 100644 --- a/handlers/admin/admin_bans.py +++ b/handlers/admin/bans/bans_handler.py @@ -5,9 +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..2a213492 --- /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 52% rename from handlers/admin/admin_servers.py rename to handlers/admin/clusters/clusters_handler.py index b1c6a67e..342d0423 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/clusters/clusters_handler.py @@ -1,45 +1,40 @@ 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, - build_clusters_editor_kb, - build_delete_server_kb, - build_manage_cluster_kb, - build_manage_server_kb, -) +from handlers.keys.key_utils import create_key_on_cluster, create_client_on_server from logger import logger - +from .keyboard import ( + build_clusters_editor_kb, + build_manage_cluster_kb, + AdminClusterCallback, + build_sync_cluster_kb, +) +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 +55,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 +97,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 +126,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 +134,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 +165,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 +201,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 +239,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 @@ -253,14 +255,14 @@ async def handle_clusters_manage( cluster_servers = servers.get(cluster_name, []) await callback_query.message.edit_text( - text=f"🔧 Управление серверами для кластера {cluster_name}", + text=f"🔧 Управление кластером {cluster_name}", reply_markup=build_manage_cluster_kb(cluster_servers, cluster_name), ) -@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 +280,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"🖥️ Проверка доступности серверов\n\n⚙️ Кластер: {cluster_name}\n\n" for server in cluster_servers: xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD, logger=logger) @@ -288,93 +290,18 @@ async def handle_servers_availability( await xui.login() online_users = len(await xui.client.online()) total_online_users += online_users - result_text += f"🌍 {server['server_name']}: {online_users} активных пользователей.\n" - + result_text += f"🌍 {server['server_name']} - онлайн: {online_users}\n" except Exception as e: - result_text += f"❌ {server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n" + result_text += f"❌ {server['server_name']} - ошибка: {e}\n" - result_text += f"\n👥 Общее количество активных пользователей в кластере: {total_online_users}." + 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 +324,81 @@ 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_sync(callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any): + cluster_name = callback_data.data + + servers = await get_servers(session) + cluster_servers = servers.get(cluster_name, []) + + await callback_query.message.answer( + text=f"🔄 Синхронизация кластера {cluster_name}", + reply_markup=build_sync_cluster_kb(cluster_servers, cluster_name), + ) + + +@router.callback_query(AdminClusterCallback.filter(F.action == "sync-server"), IsAdminFilter()) +async def handle_sync_server(callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any): + server_name = callback_data.data + + try: + query_keys = """ + SELECT s.*, k.tg_id, k.client_id, k.email, k.expiry_time + FROM servers s + JOIN keys k ON s.cluster_name = k.server_id + WHERE s.server_name = $1; + """ + keys_to_sync = await session.fetch(query_keys, server_name) + + if not keys_to_sync: + await callback_query.message.answer( + text=f"❌ Нет ключей для синхронизации в сервере {server_name}.", + reply_markup=build_admin_back_kb("clusters"), + ) + return + + text = f"🔄 Синхронизация сервера {server_name}\n\n🔑 Количество ключей: {len(keys_to_sync)}" + + await callback_query.message.answer( + text=text, + ) + + semaphore = asyncio.Semaphore(2) + for key in keys_to_sync: + try: + await create_client_on_server( + { + "api_url": key["api_url"], + "inbound_id": key["inbound_id"], + "server_name": key["server_name"], + }, + key["tg_id"], + key["client_id"], + key["email"], + key["expiry_time"], + semaphore, + ) + await asyncio.sleep(0.6) + except Exception as e: + logger.error(f"Ошибка при добавлении ключа {key['client_id']} в сервер {server_name}: {e}") + + await callback_query.message.answer( + text=f"✅ Ключи успешно синхронизированы для сервера {server_name}", + reply_markup=build_admin_back_kb("clusters"), + ) + except Exception as e: + logger.error(f"Ошибка синхронизации ключей для сервера {server_name}: {e}") + await callback_query.message.answer( + text=f"❌ Произошла ошибка при синхронизации: {e}", reply_markup=build_admin_back_kb("clusters") + ) + + +@router.callback_query(AdminClusterCallback.filter(F.action == "sync-cluster"), IsAdminFilter()) +async def handle_sync_cluster(callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any): cluster_name = callback_data.data try: @@ -418,10 +412,16 @@ 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 + text = f"🔄 Синхронизация кластера {cluster_name}\n\n🔑 Количество ключей: {len(keys_to_sync)}" + + await callback_query.message.answer( + text=text, + ) + for key in keys_to_sync: try: await create_key_on_cluster( @@ -437,59 +437,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..d761cd06 --- /dev/null +++ b/handlers/admin/clusters/keyboard.py @@ -0,0 +1,96 @@ +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 = None + + +def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + cluster_names = list(servers.keys()) + for i in range(0, len(cluster_names), 2): + builder.row(*[ + InlineKeyboardButton( + text=f"⚙️ {name}", + callback_data=AdminClusterCallback(action="manage", data=name).pack(), + ) + for name in cluster_names[i : i + 2] + ]) + + builder.row( + InlineKeyboardButton(text="➕ Добавить кластер", callback_data=AdminClusterCallback(action="add").pack()) + ) + + builder.row(build_admin_back_btn()) + + return builder.as_markup() + + +def build_manage_cluster_kb(cluster_servers: list, cluster_name: str) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + for server in cluster_servers: + builder.row( + InlineKeyboardButton( + text=f"🌍 {server['server_name']}", + callback_data=AdminServerCallback(action="manage", data=server["server_name"]).pack(), + ) + ) + + builder.row( + InlineKeyboardButton( + 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.row( + InlineKeyboardButton( + text="💾 Создать бэкап", + callback_data=AdminClusterCallback(action="backup", data=cluster_name).pack(), + ) + ) + + builder.row(build_admin_back_btn("clusters")) + return builder.as_markup() + + +def build_sync_cluster_kb(cluster_servers: list, cluster_name: str) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + for server in cluster_servers: + builder.row( + InlineKeyboardButton( + text=f"🔄 Синхронизировать {server['server_name']}", + callback_data=AdminClusterCallback(action="sync-server", data=server["server_name"]).pack(), + ) + ) + + builder.row( + InlineKeyboardButton( + text="📍 Синхронизировать кластер", + callback_data=AdminClusterCallback(action="sync-cluster", data=cluster_name).pack(), + ) + ) + + builder.row(build_admin_back_btn("clusters")) + + return builder.as_markup() diff --git a/handlers/admin/coupons/__init__.py b/handlers/admin/coupons/__init__.py new file mode 100644 index 00000000..1c00dee2 --- /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..68605fa8 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/coupons/coupons_handler.py @@ -4,14 +4,13 @@ from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, Message -from config import USERNAME_BOT +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 logger import logger - +from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb router = Router() 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..16ca5b24 --- /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..0e8a5636 --- /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: CallbackQuery, 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..7bdbaaff --- /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 68% rename from keyboards/admin/panel_kb.py rename to handlers/admin/panel/keyboard.py index 1c53a8ae..37fe725a 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: @@ -18,7 +20,7 @@ def build_panel_kb() -> InlineKeyboardMarkup: 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="clusters").pack()), InlineKeyboardButton(text="🎟️ Купоны", callback_data=AdminPanelCallback(action="coupons").pack()), ) builder.button(text="📢 Рассылка", callback_data=AdminPanelCallback(action="sender").pack()) @@ -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..9d863249 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..39fd65c6 --- /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..c60d969d 100644 --- a/handlers/admin/admin_restart.py +++ b/handlers/admin/restart/restart_handler.py @@ -4,8 +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 ..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..0172e278 --- /dev/null +++ b/handlers/admin/sender/__init__.py @@ -0,0 +1,3 @@ +__all__ = ("router",) + +from .sender_handler import router diff --git a/handlers/admin/sender/keyboard.py b/handlers/admin/sender/keyboard.py new file mode 100644 index 00000000..2bfa4606 --- /dev/null +++ b/handlers/admin/sender/keyboard.py @@ -0,0 +1,44 @@ +from aiogram.filters.callback_data import CallbackData +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder + +from ..panel.keyboard import build_admin_back_btn + + +class AdminSenderCallback(CallbackData, prefix="admin_sender"): + type: str + data: str | None = None + + +def build_sender_kb() -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + builder.row(InlineKeyboardButton(text="👥 Все пользователи", callback_data=AdminSenderCallback(type="all").pack())) + builder.row( + InlineKeyboardButton(text="✅ С подпиской", callback_data=AdminSenderCallback(type="subscribed").pack()), + InlineKeyboardButton(text="❌ Без подписки", callback_data=AdminSenderCallback(type="unsubscribed").pack()), + ) + builder.row( + InlineKeyboardButton( + text="📍 Не использовавшие триал", callback_data=AdminSenderCallback(type="untrial").pack() + ) + ) + builder.row( + InlineKeyboardButton(text="📢 Кластер", callback_data=AdminSenderCallback(type="cluster-select").pack()) + ) + builder.row(build_admin_back_btn()) + + return builder.as_markup() + + +def build_clusters_kb(clusters: list) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + + for cluster in clusters: + name = cluster["cluster_name"] + builder.button(text=f"🌐 {name}", callback_data=AdminSenderCallback(type="cluster", data=name).pack()) + + builder.adjust(2) + builder.row(build_admin_back_btn()) + + return builder.as_markup() diff --git a/handlers/admin/admin_sender.py b/handlers/admin/sender/sender_handler.py similarity index 90% rename from handlers/admin/admin_sender.py rename to handlers/admin/sender/sender_handler.py index e85338aa..7891a46e 100644 --- a/handlers/admin/admin_sender.py +++ b/handlers/admin/sender/sender_handler.py @@ -7,10 +7,9 @@ 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 .keyboard import AdminSenderCallback, build_clusters_kb, build_sender_kb from logger import logger - +from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb router = Router() @@ -97,6 +96,8 @@ async def handle_message_input(message: Message, state: FSMContext, session: Any """, int(datetime.utcnow().timestamp() * 1000), ) + elif send_to == "untrial": + tg_ids = await session.fetch("SELECT DISTINCT tg_id FROM connections WHERE trial = 0") elif send_to == "cluster": cluster_name = state_data.get("cluster_name") tg_ids = await session.fetch( @@ -115,6 +116,10 @@ async def handle_message_input(message: Message, state: FSMContext, session: Any total_users = len(tg_ids) success_count = 0 + text = f"📤 Рассылка начата!\n👥 Количество получателей: {total_users}" + + await message.answer(text=text) + for record in tg_ids: tg_id = record["tg_id"] try: @@ -131,11 +136,10 @@ async def handle_message_input(message: Message, state: FSMContext, session: Any text = ( f"📤 Рассылка завершена!\n\n" - f"👥 Всего пользователей: {total_users}\n" + f"👥 Количество получателей: {total_users}\n" f"✅ Доставлено: {success_count}\n" f"❌ Не доставлено: {total_users - success_count}" ) - await message.answer(text=text, reply_markup=build_admin_back_kb("stats"), parse_mode="HTML") - + await message.answer(text=text, reply_markup=build_admin_back_kb("sender")) await state.clear() diff --git a/handlers/admin/servers/__init__.py b/handlers/admin/servers/__init__.py new file mode 100644 index 00000000..e0d03891 --- /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..ab29be55 --- /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 + + +class AdminServerCallback(CallbackData, prefix="admin_server"): + action: str + data: str + + +def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboardMarkup: + from ..clusters.keyboard import AdminClusterCallback + + 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..e6bb8f3d --- /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..8f491060 --- /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..5afbbc14 100644 --- a/handlers/admin/admin_stats.py +++ b/handlers/admin/stats/stats_handler.py @@ -2,17 +2,15 @@ from datetime import datetime from typing import Any import pytz - from aiogram import F, Router 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 .keyboard import build_stats_kb from logger import logger from utils.csv_export import export_payments_csv, export_users_csv - +from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb router = Router() diff --git a/handlers/admin/users/__init__.py b/handlers/admin/users/__init__.py new file mode 100644 index 00000000..3dabbb0d --- /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..a01ba28e 100644 --- a/keyboards/admin/users_kb.py +++ b/handlers/admin/users/keyboard.py @@ -3,9 +3,9 @@ from datetime import datetime, timezone from aiogram.filters.callback_data import CallbackData 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 config import RENEWAL_PRICES +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 100% rename from handlers/admin/admin_users.py rename to handlers/admin/users/users_handler.py diff --git a/handlers/captcha.py b/handlers/captcha.py index 8738522b..6a494a94 100644 --- a/handlers/captcha.py +++ b/handlers/captcha.py @@ -1,20 +1,18 @@ import random import secrets - from typing import Any from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, Message from aiogram.utils.keyboard import InlineKeyboardBuilder + from config import CAPTCHA_EMOJIS from handlers.texts import CAPTCHA_PROMPT_MSG from logger import logger - from .utils import edit_or_send_message - router = Router() diff --git a/handlers/donate.py b/handlers/donate.py index 49fe0f3d..77c0d02e 100644 --- a/handlers/donate.py +++ b/handlers/donate.py @@ -3,10 +3,9 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, InlineKeyboardButton, LabeledPrice, Message, PreCheckoutQuery from aiogram.utils.keyboard import InlineKeyboardBuilder + from config import RUB_TO_XTR - from logger import logger - from .utils import edit_or_send_message diff --git a/handlers/instructions/__init__.py b/handlers/instructions/__init__.py index 3756810c..06350a7c 100644 --- a/handlers/instructions/__init__.py +++ b/handlers/instructions/__init__.py @@ -4,7 +4,6 @@ from aiogram import Router from .instructions import router as instructions_router - router = Router(name="instructions_main_router") router.include_routers( diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index 56343f64..2b35d98b 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -1,16 +1,15 @@ import os - from typing import Any -from aiogram import F, Router, types +from aiogram import F, Router from aiogram.types import ( CallbackQuery, InlineKeyboardButton, Message, ) from aiogram.utils.keyboard import InlineKeyboardBuilder -from config import CONNECT_MACOS, CONNECT_WINDOWS, 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, @@ -21,7 +20,6 @@ from handlers.texts import ( ) from handlers.utils import edit_or_send_message - router = Router() diff --git a/handlers/keys/__init__.py b/handlers/keys/__init__.py index 6a5ecfac..064024e6 100644 --- a/handlers/keys/__init__.py +++ b/handlers/keys/__init__.py @@ -5,7 +5,6 @@ from aiogram import Router from .key_management import router as management_router from .keys import router as keys_router - router = Router(name="keys_main_router") router.include_routers( diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 42f074b7..a0a48c1c 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -1,15 +1,18 @@ import asyncio import uuid - from datetime import datetime, timedelta from typing import Any import pytz - from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.types import CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder +from handlers.payments.yookassa_pay import process_custom_amount_input +from py3xui import AsyncApi + +from bot import bot +from client import delete_client from config import ( ADMIN_PASSWORD, ADMIN_USERNAME, @@ -27,10 +30,6 @@ from config import ( USE_COUNTRY_SELECTION, USE_NEW_PAYMENT_FLOW, ) -from py3xui import AsyncApi - -from bot import bot -from client import delete_client from database import ( add_connection, check_connection_exists, @@ -52,7 +51,6 @@ from handlers.buttons.add_subscribe import ( ) from handlers.keys.key_utils import create_client_on_server, create_key_on_cluster from handlers.payments.robokassa_pay import handle_custom_amount_input -from handlers.payments.yookassa_pay import process_custom_amount_input from handlers.texts import ( CREATING_CONNECTION_MSG, DISCOUNTS, @@ -64,7 +62,6 @@ from handlers.texts import ( from handlers.utils import edit_or_send_message, generate_random_email, get_least_loaded_cluster from logger import logger - router = Router() moscow_tz = pytz.timezone("Europe/Moscow") diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index ada5d1bd..d501bf8c 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -1,13 +1,14 @@ import asyncio - from typing import Any import asyncpg +from py3xui import AsyncApi from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB from py3xui import AsyncApi from client import ClientConfig, add_client, delete_client, extend_client_key, get_client_traffic, toggle_client +from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, DATABASE_URL 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 9c3d0517..9f03d393 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -2,16 +2,17 @@ import asyncio import locale import os import time - from datetime import datetime, timedelta from typing import Any import asyncpg import pytz - from aiogram import F, Router, types from aiogram.types import CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder +from handlers.payments.yookassa_pay import process_custom_amount_input + +from bot import bot from config import ( CONNECT_ANDROID, CONNECT_IOS, @@ -23,13 +24,11 @@ from config import ( ENABLE_UPDATE_SUBSCRIPTION_BUTTON, PUBLIC_LINK, RENEWAL_PLANS, - TOGGLE_CLIENT, TOTAL_GB, USE_COUNTRY_SELECTION, USE_NEW_PAYMENT_FLOW, + TOGGLE_CLIENT, ) - -from bot import bot from database import ( check_server_name_by_cluster, create_temporary_data, @@ -53,32 +52,30 @@ from handlers.buttons.add_subscribe import ( from handlers.keys.key_utils import ( delete_key_from_cluster, renew_key_in_cluster, - toggle_client_on_cluster, update_subscription, + toggle_client_on_cluster, ) from handlers.payments.robokassa_pay import handle_custom_amount_input -from handlers.payments.yookassa_pay import process_custom_amount_input from handlers.texts import ( - DELETE_KEY_CONFIRM_MSG, DISCOUNTS, - FREEZE_SUBSCRIPTION_CONFIRM_MSG, - FROZEN_SUBSCRIPTION_MSG, - INSUFFICIENT_FUNDS_RENEWAL_MSG, - KEY_DELETED_MSG_SIMPLE, KEY_NOT_FOUND_MSG, - NO_SUBSCRIPTIONS_MSG, PLAN_SELECTION_MSG, SUBSCRIPTION_DESCRIPTION, - SUBSCRIPTION_FROZEN_MSG, - SUBSCRIPTION_UNFROZEN_MSG, SUCCESS_RENEWAL_MSG, - UNFREEZE_SUBSCRIPTION_CONFIRM_MSG, key_message, + NO_SUBSCRIPTIONS_MSG, + FROZEN_SUBSCRIPTION_MSG, + UNFREEZE_SUBSCRIPTION_CONFIRM_MSG, + SUBSCRIPTION_UNFROZEN_MSG, + FREEZE_SUBSCRIPTION_CONFIRM_MSG, + SUBSCRIPTION_FROZEN_MSG, + DELETE_KEY_CONFIRM_MSG, + KEY_DELETED_MSG_SIMPLE, + INSUFFICIENT_FUNDS_RENEWAL_MSG, ) from handlers.utils import edit_or_send_message, handle_error from logger import logger - locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8") router = Router() @@ -290,6 +287,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( @@ -384,6 +382,7 @@ async def process_callback_freeze_subscription(callback_query: CallbackQuery, se """ Показывает пользователю диалог подтверждения заморозки (отключения) подписки. """ + tg_id = callback_query.message.chat.id key_name = callback_query.data.split("|")[1] confirm_text = FREEZE_SUBSCRIPTION_CONFIRM_MSG @@ -433,7 +432,7 @@ async def process_callback_freeze_subscription_confirm(callback_query: CallbackQ if time_left < 0: time_left = 0 - await session.execute( + update_result = await session.execute( """ UPDATE keys SET expiry_time = $1, diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index 412fd6c3..bfd5123a 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -4,18 +4,16 @@ import random import re import time import urllib.parse - from datetime import datetime import aiohttp import asyncpg import pytz - from aiohttp import web + from config import ( DATABASE_URL, PROJECT_NAME, - SUB_MESSAGE, SUPERNODE, SUPPORT_CHAT_URL, TOTAL_GB, @@ -23,7 +21,6 @@ from config import ( USERNAME_BOT, USE_COUNTRY_SELECTION, ) - from database import get_key_details, get_servers from handlers.utils import convert_to_bytes from logger import logger diff --git a/handlers/notifications/__init__.py b/handlers/notifications/__init__.py index 6b90acab..ee8f833d 100644 --- a/handlers/notifications/__init__.py +++ b/handlers/notifications/__init__.py @@ -5,7 +5,6 @@ from aiogram import Router from .general_notifications import router as general_notifications_router from .special_notifications import router as special_notifications_router - router = Router(name="notifications_main_router") router.include_routers(general_notifications_router, special_notifications_router) diff --git a/handlers/notifications/general_notifications.py b/handlers/notifications/general_notifications.py index 7d330c7c..f5669ae1 100644 --- a/handlers/notifications/general_notifications.py +++ b/handlers/notifications/general_notifications.py @@ -1,25 +1,23 @@ import asyncio - from datetime import datetime, timedelta import asyncpg import pytz - from aiogram import Bot, Router + from config import ( DATABASE_URL, NOTIFICATION_TIME, NOTIFY_DELETE_DELAY, NOTIFY_DELETE_KEY, - NOTIFY_INACTIVE_TRAFFIC, NOTIFY_MAXPRICE, NOTIFY_RENEW, NOTIFY_RENEW_EXPIRED, RENEWAL_PRICES, TOTAL_GB, TRIAL_TIME_DISABLE, + NOTIFY_INACTIVE_TRAFFIC, ) - from database import ( add_notification, check_notification_time, @@ -32,23 +30,21 @@ from database import ( ) from handlers.keys.key_utils import delete_key_from_cluster, renew_key_in_cluster from handlers.texts import ( + KEY_EXPIRY_10H, + KEY_EXPIRY_24H, + KEY_RENEWED, + KEY_RENEWED_TEMP_MSG, KEY_DELETED_MSG, KEY_EXPIRED_DELAY_HOURS_MINUTES_MSG, KEY_EXPIRED_DELAY_HOURS_MSG, KEY_EXPIRED_DELAY_MINUTES_MSG, KEY_EXPIRED_NO_DELAY_MSG, - KEY_EXPIRY_10H, - KEY_EXPIRY_24H, - KEY_RENEWED, - KEY_RENEWED_TEMP_MSG, ) from keyboards.notifications.notify_kb import build_notification_expired_kb, build_notification_kb from logger import logger - from .notify_utils import send_notification from .special_notifications import notify_inactive_trial_users, notify_users_no_traffic - router = Router() moscow_tz = pytz.timezone("Europe/Moscow") diff --git a/handlers/notifications/notify_utils.py b/handlers/notifications/notify_utils.py index ffabbcee..4a9a9992 100644 --- a/handlers/notifications/notify_utils.py +++ b/handlers/notifications/notify_utils.py @@ -1,7 +1,6 @@ import os import aiofiles - from aiogram import Bot from aiogram.exceptions import TelegramForbiddenError from aiogram.types import BufferedInputFile, InlineKeyboardMarkup diff --git a/handlers/notifications/special_notifications.py b/handlers/notifications/special_notifications.py index a63c1ea8..a46e3e5f 100644 --- a/handlers/notifications/special_notifications.py +++ b/handlers/notifications/special_notifications.py @@ -1,25 +1,22 @@ import asyncio - from datetime import datetime, timedelta import asyncpg import pytz - from aiogram import Bot, Router, types from aiogram.exceptions import TelegramForbiddenError from aiogram.utils.keyboard import InlineKeyboardBuilder -from config import NOTIFY_EXTRA_DAYS, NOTIFY_INACTIVE, NOTIFY_INACTIVE_TRAFFIC, SUPPORT_CHAT_URL, TRIAL_TIME +from config import NOTIFY_EXTRA_DAYS, NOTIFY_INACTIVE, NOTIFY_INACTIVE_TRAFFIC, SUPPORT_CHAT_URL, TRIAL_TIME from database import ( add_notification, check_notification_time, create_blocked_user, ) from handlers.keys.key_utils import get_user_traffic -from handlers.texts import TRIAL_INACTIVE_BONUS_MSG, TRIAL_INACTIVE_FIRST_MSG, ZERO_TRAFFIC_MSG +from handlers.texts import TRIAL_INACTIVE_FIRST_MSG, TRIAL_INACTIVE_BONUS_MSG, ZERO_TRAFFIC_MSG from logger import logger - router = Router() moscow_tz = pytz.timezone("Europe/Moscow") diff --git a/handlers/pay.py b/handlers/pay.py index d922ae21..9f40047a 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -1,6 +1,7 @@ from aiogram import F, Router from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder + from config import ( CRYPTO_BOT_ENABLE, DONATIONS_ENABLE, @@ -14,7 +15,6 @@ from handlers.texts import PAYMENT_METHODS_MSG from .utils import edit_or_send_message - router = Router() diff --git a/handlers/payments/__init__.py b/handlers/payments/__init__.py index ba0f040b..9009f7cb 100644 --- a/handlers/payments/__init__.py +++ b/handlers/payments/__init__.py @@ -1,6 +1,7 @@ __all__ = ("router",) from aiogram import Router + from config import ( CRYPTO_BOT_ENABLE, ROBOKASSA_ENABLE, @@ -8,7 +9,6 @@ from config import ( YOOKASSA_ENABLE, YOOMONEY_ENABLE, ) - from .cryprobot_pay import router as cryprobot_router from .gift import router as gift_router from .robokassa_pay import router as robokassa_router diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index 9437e05d..f1568b1d 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -8,6 +8,9 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web +from handlers.payments.utils import send_payment_success_notification +from robokassa import HashAlgorithm, Robokassa + from config import ( DATABASE_URL, ROBOKASSA_ENABLE, @@ -16,8 +19,6 @@ from config import ( ROBOKASSA_PASSWORD2, ROBOKASSA_TEST_MODE, ) -from robokassa import HashAlgorithm, Robokassa - from database import ( add_connection, add_payment, @@ -26,7 +27,6 @@ from database import ( get_temporary_data, update_balance, ) -from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from handlers.utils import edit_or_send_message from logger import logger diff --git a/handlers/profile.py b/handlers/profile.py index e2083c6c..2fb27cda 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -1,10 +1,8 @@ import html import os - from typing import Any import asyncpg - from aiogram import F, Router from aiogram.enums import ParseMode from aiogram.fsm.context import FSMContext @@ -17,6 +15,7 @@ from aiogram.types import ( Message, ) from aiogram.utils.keyboard import InlineKeyboardBuilder + from config import ( DATABASE_URL, INLINE_MODE, @@ -27,7 +26,6 @@ from config import ( TRIAL_TIME, USERNAME_BOT, ) - from database import get_balance, get_key_count, get_last_payments, get_referral_stats, get_trial from handlers.buttons.profile import ( ADD_SUB, @@ -40,20 +38,11 @@ from handlers.buttons.profile import ( MY_SUBS, PAYMENT, ) -from handlers.texts import ( - BALANCE_HISTORY_HEADER, - BALANCE_MANAGEMENT_TEXT, - INVITE_TEXT_NON_INLINE, - get_referral_link, - invite_message_send, - profile_message_send, -) -from keyboards.admin.panel_kb import AdminPanelCallback from logger import logger - +from .admin.panel.keyboard import AdminPanelCallback +from .texts import profile_message_send, invite_message_send, get_referral_link from .utils import edit_or_send_message - router = Router() diff --git a/handlers/start.py b/handlers/start.py index 3b38f344..0332863d 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -11,6 +11,8 @@ from aiogram.types import ( Message, ) from aiogram.utils.keyboard import InlineKeyboardBuilder + +from bot import bot from config import ( CAPTCHA_ENABLE, CHANNEL_EXISTS, @@ -20,8 +22,6 @@ from config import ( DONATIONS_ENABLE, SUPPORT_CHAT_URL, ) - -from bot import bot from database import ( add_connection, add_referral, @@ -44,12 +44,10 @@ from handlers.texts import ( WELCOME_TEXT, get_about_vpn, ) -from keyboards.admin.panel_kb import AdminPanelCallback from logger import logger - +from .admin.panel.keyboard import AdminPanelCallback from .utils import edit_or_send_message - router = Router() diff --git a/handlers/utils.py b/handlers/utils.py index 4020c904..871f8228 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -7,11 +7,10 @@ import string import aiofiles import aiohttp import asyncpg - from aiogram.types import BufferedInputFile, InlineKeyboardMarkup, InputMediaPhoto, Message -from config import DATABASE_URL from bot import bot +from config import DATABASE_URL from database import get_all_keys, get_servers 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/sender_kb.py b/keyboards/admin/sender_kb.py deleted file mode 100644 index 82c37115..00000000 --- a/keyboards/admin/sender_kb.py +++ /dev/null @@ -1,32 +0,0 @@ -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 - - -class AdminSenderCallback(CallbackData, prefix="admin_sender"): - type: str - data: str | None = None - - -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.button(text="📢 Пользователи кластера", callback_data=AdminSenderCallback(type="cluster-select").pack()) - builder.row(build_admin_back_btn()) - builder.adjust(1) - return builder.as_markup() - - -def build_clusters_kb(clusters: list) -> InlineKeyboardMarkup: - builder = InlineKeyboardBuilder() - for cluster in clusters: - name = cluster["cluster_name"] - builder.button(text=f"🌐 {name}", callback_data=AdminSenderCallback(type="cluster", data=name).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 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/logger.py b/logger.py index 91c16f53..7130c5e9 100644 --- a/logger.py +++ b/logger.py @@ -1,12 +1,10 @@ import logging import os import sys - from datetime import timedelta from loguru import logger - log_folder = "logs" if not os.path.exists(log_folder): diff --git a/middlewares/__init__.py b/middlewares/__init__.py index f2b1d9d9..de2c7b64 100644 --- a/middlewares/__init__.py +++ b/middlewares/__init__.py @@ -1,5 +1,4 @@ from collections.abc import Iterable -from typing import Any, Optional, Type, Union from aiogram import Dispatcher from aiogram.dispatcher.middlewares.base import BaseMiddleware diff --git a/middlewares/admin.py b/middlewares/admin.py index de702114..65474882 100644 --- a/middlewares/admin.py +++ b/middlewares/admin.py @@ -3,6 +3,7 @@ from typing import Any from aiogram import BaseMiddleware from aiogram.types import CallbackQuery, Message, TelegramObject + from config import ADMIN_ID diff --git a/middlewares/session.py b/middlewares/session.py index 712b01fe..78266449 100644 --- a/middlewares/session.py +++ b/middlewares/session.py @@ -2,9 +2,9 @@ from collections.abc import Awaitable, Callable from typing import Any import asyncpg - from aiogram import BaseMiddleware from aiogram.types import TelegramObject + from config import DATABASE_URL diff --git a/servers.py b/servers.py index b403e24f..f9728da0 100644 --- a/servers.py +++ b/servers.py @@ -1,19 +1,17 @@ import asyncio import re - from datetime import datetime, timedelta from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -from config import ADMIN_ID, PING_TIME from ping3 import ping from bot import bot +from config import ADMIN_ID, PING_TIME from database import get_servers -from keyboards.admin.servers_kb import AdminServerEditorCallback +from handlers.admin.servers.keyboard import AdminServerCallback from logger import logger - last_ping_times = {} last_down_times = {} notified_servers = set() @@ -49,7 +47,7 @@ async def notify_admin(server_name: str, status: str, down_duration: timedelta = builder.row( InlineKeyboardButton( text="Управление сервером", - callback_data=AdminServerEditorCallback(action="servers_manage", data=server_name).pack(), + callback_data=AdminServerCallback(action="manage", data=server_name).pack(), ) ) diff --git a/utils/csv_export.py b/utils/csv_export.py index d4f2ed30..6023c1ae 100644 --- a/utils/csv_export.py +++ b/utils/csv_export.py @@ -1,5 +1,4 @@ import csv - from io import StringIO from typing import Any