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)