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