From 77bd87a2b5a975eecd287a4443b756e63c86947c Mon Sep 17 00:00:00 2001 From: hteppl Date: Tue, 25 Mar 2025 16:22:52 +0300 Subject: [PATCH 01/16] Rename classes + implement own packages for all handlers --- handlers/admin/__init__.py | 19 +++++++++---------- handlers/admin/backups/__init__.py | 3 +++ .../backups_handler.py} | 0 handlers/admin/bans/__init__.py | 3 +++ .../{admin_bans.py => bans/bans_handler.py} | 0 handlers/admin/coupons/__init__.py | 3 +++ .../coupons_handler.py} | 0 handlers/admin/panel/__init__.py | 3 +++ .../panel_handler.py} | 0 handlers/admin/restart/__init__.py | 3 +++ .../restart_handler.py} | 0 handlers/admin/sender/__init__.py | 3 +++ .../sender_handler.py} | 0 handlers/admin/servers/__init__.py | 3 +++ .../servers_handler.py} | 0 handlers/admin/stats/__init__.py | 3 +++ .../stats_handler.py} | 0 handlers/admin/users/__init__.py | 3 +++ .../users_handler.py} | 0 19 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 handlers/admin/backups/__init__.py rename handlers/admin/{admin_backups.py => backups/backups_handler.py} (100%) create mode 100644 handlers/admin/bans/__init__.py rename handlers/admin/{admin_bans.py => bans/bans_handler.py} (100%) create mode 100644 handlers/admin/coupons/__init__.py rename handlers/admin/{admin_coupons.py => coupons/coupons_handler.py} (100%) create mode 100644 handlers/admin/panel/__init__.py rename handlers/admin/{admin_panel.py => panel/panel_handler.py} (100%) create mode 100644 handlers/admin/restart/__init__.py rename handlers/admin/{admin_restart.py => restart/restart_handler.py} (100%) create mode 100644 handlers/admin/sender/__init__.py rename handlers/admin/{admin_sender.py => sender/sender_handler.py} (100%) create mode 100644 handlers/admin/servers/__init__.py rename handlers/admin/{admin_servers.py => servers/servers_handler.py} (100%) create mode 100644 handlers/admin/stats/__init__.py rename handlers/admin/{admin_stats.py => stats/stats_handler.py} (100%) create mode 100644 handlers/admin/users/__init__.py rename handlers/admin/{admin_users.py => users/users_handler.py} (100%) diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py index 50669db1..de5256be 100644 --- a/handlers/admin/__init__.py +++ b/handlers/admin/__init__.py @@ -2,16 +2,15 @@ __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 .coupons import router as coupons_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") 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 100% rename from handlers/admin/admin_backups.py rename to handlers/admin/backups/backups_handler.py 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 100% rename from handlers/admin/admin_bans.py rename to handlers/admin/bans/bans_handler.py 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 100% rename from handlers/admin/admin_coupons.py rename to handlers/admin/coupons/coupons_handler.py 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/handlers/admin/admin_panel.py b/handlers/admin/panel/panel_handler.py similarity index 100% rename from handlers/admin/admin_panel.py rename to handlers/admin/panel/panel_handler.py 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 100% rename from handlers/admin/admin_restart.py rename to handlers/admin/restart/restart_handler.py 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/handlers/admin/admin_sender.py b/handlers/admin/sender/sender_handler.py similarity index 100% rename from handlers/admin/admin_sender.py rename to handlers/admin/sender/sender_handler.py 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/admin_servers.py b/handlers/admin/servers/servers_handler.py similarity index 100% rename from handlers/admin/admin_servers.py rename to handlers/admin/servers/servers_handler.py 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/handlers/admin/admin_stats.py b/handlers/admin/stats/stats_handler.py similarity index 100% rename from handlers/admin/admin_stats.py rename to handlers/admin/stats/stats_handler.py 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/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 From 5cd63bc89ddd9c22e93c99ea835bd75a74e167de Mon Sep 17 00:00:00 2001 From: hteppl Date: Tue, 25 Mar 2025 16:28:08 +0300 Subject: [PATCH 02/16] Move admin keyboards to their own packages --- handlers/admin/backups/backups_handler.py | 2 +- handlers/admin/bans/bans_handler.py | 4 ++-- keyboards/admin/bans_kb.py => handlers/admin/bans/keyboard.py | 2 +- handlers/admin/coupons/coupons_handler.py | 4 ++-- .../admin/coupons_kb.py => handlers/admin/coupons/keyboard.py | 2 +- .../admin/panel_kb.py => handlers/admin/panel/keyboard.py | 0 handlers/admin/panel/panel_handler.py | 2 +- handlers/admin/restart/restart_handler.py | 2 +- .../admin/sender_kb.py => handlers/admin/sender/keyboard.py | 2 +- handlers/admin/sender/sender_handler.py | 4 ++-- .../admin/servers_kb.py => handlers/admin/servers/keyboard.py | 2 +- handlers/admin/servers/servers_handler.py | 4 ++-- .../admin/stats_kb.py => handlers/admin/stats/keyboard.py | 2 +- handlers/admin/stats/stats_handler.py | 4 ++-- .../admin/users_kb.py => handlers/admin/users/keyboard.py | 2 +- handlers/admin/users/users_handler.py | 2 +- handlers/profile.py | 2 +- handlers/start.py | 2 +- keyboards/admin/__init__.py | 0 19 files changed, 22 insertions(+), 22 deletions(-) rename keyboards/admin/bans_kb.py => handlers/admin/bans/keyboard.py (87%) rename keyboards/admin/coupons_kb.py => handlers/admin/coupons/keyboard.py (95%) rename keyboards/admin/panel_kb.py => handlers/admin/panel/keyboard.py (100%) rename keyboards/admin/sender_kb.py => handlers/admin/sender/keyboard.py (95%) rename keyboards/admin/servers_kb.py => handlers/admin/servers/keyboard.py (97%) rename keyboards/admin/stats_kb.py => handlers/admin/stats/keyboard.py (90%) rename keyboards/admin/users_kb.py => handlers/admin/users/keyboard.py (99%) delete mode 100644 keyboards/admin/__init__.py diff --git a/handlers/admin/backups/backups_handler.py b/handlers/admin/backups/backups_handler.py index 69ad7cc9..35d9c0e8 100644 --- a/handlers/admin/backups/backups_handler.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/bans_handler.py b/handlers/admin/bans/bans_handler.py index 44379b5a..28f44213 100644 --- a/handlers/admin/bans/bans_handler.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/coupons/coupons_handler.py b/handlers/admin/coupons/coupons_handler.py index b3bf4164..1c7bbe92 100644 --- a/handlers/admin/coupons/coupons_handler.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/keyboards/admin/panel_kb.py b/handlers/admin/panel/keyboard.py similarity index 100% rename from keyboards/admin/panel_kb.py rename to handlers/admin/panel/keyboard.py diff --git a/handlers/admin/panel/panel_handler.py b/handlers/admin/panel/panel_handler.py index d940c2c7..6e244516 100644 --- a/handlers/admin/panel/panel_handler.py +++ b/handlers/admin/panel/panel_handler.py @@ -5,7 +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_management_kb, build_panel_kb router = Router() diff --git a/handlers/admin/restart/restart_handler.py b/handlers/admin/restart/restart_handler.py index 9acc1195..12b4c013 100644 --- a/handlers/admin/restart/restart_handler.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/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/sender/sender_handler.py b/handlers/admin/sender/sender_handler.py index e85338aa..bfe33492 100644 --- a/handlers/admin/sender/sender_handler.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/keyboards/admin/servers_kb.py b/handlers/admin/servers/keyboard.py similarity index 97% rename from keyboards/admin/servers_kb.py rename to handlers/admin/servers/keyboard.py index e7d148e8..762c697d 100644 --- a/keyboards/admin/servers_kb.py +++ b/handlers/admin/servers/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 AdminServerEditorCallback(CallbackData, prefix="admin_servers"): diff --git a/handlers/admin/servers/servers_handler.py b/handlers/admin/servers/servers_handler.py index b1c6a67e..5ea58cd6 100644 --- a/handlers/admin/servers/servers_handler.py +++ b/handlers/admin/servers/servers_handler.py @@ -15,8 +15,8 @@ from backup import create_backup_and_send_to_admins from database import check_unique_server_name, delete_server, 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 ( +from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb +from keyboard import ( AdminServerEditorCallback, build_clusters_editor_kb, build_delete_server_kb, 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/stats/stats_handler.py b/handlers/admin/stats/stats_handler.py index 042036c7..92e68c33 100644 --- a/handlers/admin/stats/stats_handler.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/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/users/users_handler.py b/handlers/admin/users/users_handler.py index 9e658fb9..c95780b1 100644 --- a/handlers/admin/users/users_handler.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, diff --git a/handlers/profile.py b/handlers/profile.py index 24f1ac2d..a76a4328 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -41,7 +41,7 @@ from handlers.buttons.profile import ( PAYMENT, ) from handlers.texts import get_referral_link, invite_message_send, profile_message_send -from keyboards.admin.panel_kb import AdminPanelCallback +from ..panel.keyboard import AdminPanelCallback from logger import logger from .utils import edit_or_send_message diff --git a/handlers/start.py b/handlers/start.py index e76c7514..7dd91283 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -33,7 +33,7 @@ from database import ( from handlers.captcha import generate_captcha from handlers.keys.key_management import create_key from handlers.texts import WELCOME_TEXT, get_about_vpn -from keyboards.admin.panel_kb import AdminPanelCallback +from ..panel.keyboard import AdminPanelCallback from logger import logger from .utils import edit_or_send_message diff --git a/keyboards/admin/__init__.py b/keyboards/admin/__init__.py deleted file mode 100644 index e69de29b..00000000 From 472a3ad69d1cffd184b31343b40943de5d305c4a Mon Sep 17 00:00:00 2001 From: hteppl Date: Tue, 25 Mar 2025 16:30:44 +0300 Subject: [PATCH 03/16] Use ruff formatting --- client.py | 9 ++- handlers/admin/users/users_handler.py | 16 ++--- handlers/buttons/yookassa.py | 2 +- handlers/keys/keys.py | 65 ++++++------------- .../notifications/general_notifications.py | 2 +- middlewares/loggings.py | 2 +- middlewares/throttling.py | 2 +- servers.py | 13 ++-- utils/csv_export.py | 6 +- 9 files changed, 47 insertions(+), 70 deletions(-) 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/users/users_handler.py b/handlers/admin/users/users_handler.py index c95780b1..db48c12e 100644 --- a/handlers/admin/users/users_handler.py +++ b/handlers/admin/users/users_handler.py @@ -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 e41a6b6d..1c603573 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 @@ -146,10 +146,7 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any) is_frozen = record["is_frozen"] if is_frozen: - response_message = ( - "Подписка заморожена.\n" - "Дата истечения будет обновлена после разморозки." - ) + response_message = "Подписка заморожена.\nДата истечения будет обновлена после разморозки." builder = InlineKeyboardBuilder() builder.row( @@ -174,7 +171,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"] @@ -185,17 +182,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( @@ -245,18 +238,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: @@ -297,8 +284,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 = ( - "Хотите включить (разморозить) подписку?\n\n" - "После включения доступа трафик и время снова начнут расходоваться." + "Хотите включить (разморозить) подписку?\n\nПосле включения доступа трафик и время снова начнут расходоваться." ) builder = InlineKeyboardBuilder() @@ -356,7 +342,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( @@ -364,16 +350,11 @@ async def process_callback_unfreeze_subscription_confirm(callback_query: Callbac email=email, client_id=client_id, new_expiry_time=new_expiry_time, - total_gb=TOTAL_GB - ) - text_ok = ( - "✅ Подписка успешно включена.\n\n" - "Теперь трафик и время подписки будут расходоваться." + total_gb=TOTAL_GB, ) + text_ok = "✅ Подписка успешно включена.\n\nТеперь трафик и время подписки будут расходоваться." 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, @@ -381,13 +362,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, @@ -455,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( """ @@ -467,7 +445,7 @@ async def process_callback_freeze_subscription_confirm(callback_query: CallbackQ """, time_left, record["tg_id"], - client_id + client_id, ) text_ok = ( @@ -475,9 +453,7 @@ async def process_callback_freeze_subscription_confirm(callback_query: CallbackQ "Чтобы включить обратно, зайдите в меню ключа и нажмите «Включить подписку»." ) 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, @@ -485,13 +461,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 2529cf48..2895f305 100644 --- a/handlers/notifications/general_notifications.py +++ b/handlers/notifications/general_notifications.py @@ -17,7 +17,7 @@ from config import ( RENEWAL_PRICES, TOTAL_GB, TRIAL_TIME_DISABLE, - NOTIFY_INACTIVE_TRAFFIC + NOTIFY_INACTIVE_TRAFFIC, ) from database import ( 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) From 71301f90ae3bfd3c20b1eee1927521393aaed25a Mon Sep 17 00:00:00 2001 From: hteppl Date: Tue, 25 Mar 2025 16:32:31 +0300 Subject: [PATCH 04/16] Update AdminPanelCallback __init__ as in super().__init__ --- handlers/admin/panel/keyboard.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/handlers/admin/panel/keyboard.py b/handlers/admin/panel/keyboard.py index 1c53a8ae..f897e34f 100644 --- a/handlers/admin/panel/keyboard.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: From 6b8507253e8b687872b7180a9f63d27c87216157 Mon Sep 17 00:00:00 2001 From: hteppl Date: Tue, 25 Mar 2025 16:52:38 +0300 Subject: [PATCH 05/16] Improve clusters and servers manage texts + fix input len checks --- handlers/admin/servers/servers_handler.py | 46 ++++++++++++++--------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/handlers/admin/servers/servers_handler.py b/handlers/admin/servers/servers_handler.py index 5ea58cd6..b619961d 100644 --- a/handlers/admin/servers/servers_handler.py +++ b/handlers/admin/servers/servers_handler.py @@ -66,7 +66,8 @@ async def handle_servers(callback_query: CallbackQuery): async def handle_clusters_add(callback_query: CallbackQuery, state: FSMContext): text = ( "🔧 Введите имя нового кластера:\n\n" - "Имя кластера должно быть уникальным!\n" + "Имя должно быть уникальным!\n" + "Имя не должно превышать 12 символов!\n" "Пример: cluster1 или us_east_1" ) @@ -79,7 +80,13 @@ async def handle_clusters_add(callback_query: CallbackQuery, state: FSMContext): 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("servers") + ) + return + + if len(message.text) > 12: + await message.answer( + text="❌ Имя кластера должно превышать 12 символов! Попробуйте снова.", reply_markup=build_admin_back_kb("servers") ) return @@ -104,15 +111,16 @@ async def handle_cluster_name_input(message: Message, state: FSMContext): 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("servers") ) return server_name = message.text.strip() - if len(server_name) > 14: + if len(server_name) > 12: await message.answer( - text="❌ Имя сервера не должно превышать 10 символов. Попробуйте снова.", + text="❌ Имя сервера не должно превышать 12 символов. Попробуйте снова.", reply_markup=build_admin_back_kb("servers"), ) return @@ -131,9 +139,9 @@ 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( @@ -145,7 +153,7 @@ async def handle_server_name_input(message: Message, state: FSMContext, session: @router.message(AdminServersEditor.waiting_for_api_url, IsAdminFilter()) -async def handle_api_url_input(message: Message, state: FSMContext, session: Any): +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://. Попробуйте снова.", @@ -162,10 +170,9 @@ 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( @@ -278,8 +285,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,7 +296,6 @@ 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" @@ -359,9 +365,13 @@ async def handle_servers_add( await state.update_data(cluster_name=cluster_name) text = ( - f"Введите имя сервера для кластера {cluster_name}:\n\n" - "Рекомендуется указать локацию сервера в имени.\n\n" - "Пример: server-asia, server-europe" + f"✏️ Введите название нового сервера для кластера {cluster_name}:\n\n" + "🌍 Рекомендуется использовать локацию и порядковый номер сервера в названии.\n" + "Имя не должно превышать 12 символов!\n\n" + "Пример:\n" + "— de1 - для первого сервера в Германии\n" + "— swe2 - для второго сервера в Швеции\n" + "(если в каком-то кластере у вас уже есть сервер в Швеции)" ) await callback_query.message.edit_text( From 3d10e2c1fe9829e6af9bbe85d539d8cdeff151dd Mon Sep 17 00:00:00 2001 From: hteppl Date: Tue, 25 Mar 2025 16:54:02 +0300 Subject: [PATCH 06/16] Code optimize by ruff --- handlers/admin/servers/servers_handler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/handlers/admin/servers/servers_handler.py b/handlers/admin/servers/servers_handler.py index b619961d..2b58393f 100644 --- a/handlers/admin/servers/servers_handler.py +++ b/handlers/admin/servers/servers_handler.py @@ -86,7 +86,8 @@ async def handle_cluster_name_input(message: Message, state: FSMContext): if len(message.text) > 12: await message.answer( - text="❌ Имя кластера должно превышать 12 символов! Попробуйте снова.", reply_markup=build_admin_back_kb("servers") + text="❌ Имя кластера должно превышать 12 символов! Попробуйте снова.", + reply_markup=build_admin_back_kb("servers"), ) return @@ -111,8 +112,7 @@ async def handle_cluster_name_input(message: Message, state: FSMContext): 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("servers") ) return From 5f4a23e80807a5a84051b7bed4f47658c90cb0aa Mon Sep 17 00:00:00 2001 From: hteppl Date: Tue, 25 Mar 2025 17:12:47 +0300 Subject: [PATCH 07/16] Trying to split cluster and server manage logic --- handlers/admin/__init__.py | 2 + handlers/admin/clusters/__init__.py | 3 + handlers/admin/clusters/clusters_handler.py | 421 ++++++++++++++++++ handlers/admin/clusters/keyboard.py | 58 +++ handlers/admin/servers/keyboard.py | 61 +-- handlers/admin/servers/servers_handler.py | 462 +------------------- 6 files changed, 503 insertions(+), 504 deletions(-) create mode 100644 handlers/admin/clusters/__init__.py create mode 100644 handlers/admin/clusters/clusters_handler.py create mode 100644 handlers/admin/clusters/keyboard.py diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py index de5256be..f77eff4d 100644 --- a/handlers/admin/__init__.py +++ b/handlers/admin/__init__.py @@ -4,6 +4,7 @@ from aiogram import 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 .panel import router as panel_router from .restart import router as restart_router @@ -17,6 +18,7 @@ router = Router(name="admins_main_router") router.include_routers( panel_router, servers_router, + clusters_router, users_router, stats_router, backups_router, 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/clusters/clusters_handler.py b/handlers/admin/clusters/clusters_handler.py new file mode 100644 index 00000000..31496a69 --- /dev/null +++ b/handlers/admin/clusters/clusters_handler.py @@ -0,0 +1,421 @@ +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 py3xui import AsyncApi + +from backup import create_backup_and_send_to_admins +from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL +from database import check_unique_server_name, get_servers +from filters.admin import IsAdminFilter +from handlers.keys.key_utils import create_key_on_cluster +from keyboard import ( + build_clusters_editor_kb, + build_manage_cluster_kb, AdminClusterCallback, +) +from logger import logger +from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb + +router = Router() + + +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 == "clusters"), + IsAdminFilter(), +) +async def handle_servers(callback_query: CallbackQuery): + servers = await get_servers() + + text = ( + "🔧 Управление кластерами\n\n" + "📌 Здесь вы можете добавить новый кластер.\n\n" + "🌐 Кластеры — это пространство серверов, в пределах которого создается подписка.\n" + "💡 Если вы хотите выдавать по 1 серверу, то добавьте всего 1 сервер в кластер.\n\n" + "⚠️ Важно: Кластеры удаляются автоматически, если удалить все серверы внутри них.\n\n" + ) + + await callback_query.message.edit_text( + text=text, + reply_markup=build_clusters_editor_kb(servers), + ) + + +@router.callback_query( + AdminClusterCallback.filter(F.action == "add"), + IsAdminFilter(), +) +async def handle_clusters_add(callback_query: CallbackQuery, state: FSMContext): + text = ( + "🔧 Введите имя нового кластера:\n\n" + "Имя должно быть уникальным!\n" + "Имя не должно превышать 12 символов!\n" + "Пример: cluster1 или us_east_1" + ) + + await callback_query.message.edit_text(text=text, reply_markup=build_admin_back_kb("clusters")) + + await state.set_state(AdminClusterStates.waiting_for_cluster_name) + + +@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("clusters") + ) + return + + if len(message.text) > 12: + await message.answer( + text="❌ Имя кластера должно превышать 12 символов! Попробуйте снова.", + reply_markup=build_admin_back_kb("clusters"), + ) + return + + cluster_name = message.text.strip() + await state.update_data(cluster_name=cluster_name) + + text = ( + f"Введите имя сервера для кластера {cluster_name}:\n\n" + "Рекомендуется указать локацию и номер сервера в имени.\n\n" + "Пример: de1, fra1, fi2" + ) + + await message.answer( + text=text, + reply_markup=build_admin_back_kb("clusters"), + ) + + await state.set_state(AdminClusterStates.waiting_for_server_name) + + +@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("clusters") + ) + return + + server_name = message.text.strip() + + if len(server_name) > 12: + await message.answer( + text="❌ Имя сервера не должно превышать 12 символов. Попробуйте снова.", + reply_markup=build_admin_back_kb("clusters"), + ) + return + + user_data = await state.get_data() + cluster_name = user_data.get("cluster_name") + + if not await check_unique_server_name(server_name, session, cluster_name): + await message.answer( + text="❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя.", + reply_markup=build_admin_back_kb("clusters"), + ) + return + + await state.update_data(server_name=server_name) + + text = ( + f"Введите API URL для сервера {server_name} в кластере {cluster_name}:\n\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("clusters"), + ) + + await state.set_state(AdminClusterStates.waiting_for_api_url) + + +@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("clusters"), + ) + return + + api_url = message.text.strip().rstrip("/") + + user_data = await state.get_data() + cluster_name = user_data.get("cluster_name") + server_name = user_data.get("server_name") + await state.update_data(api_url=api_url) + + text = ( + f"Введите subscription_url для сервера {server_name} в кластере {cluster_name}:\n\n" + "Ссылку можно найти в панели 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("clusters"), + ) + + await state.set_state(AdminClusterStates.waiting_for_subscription_url) + + +@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("clusters"), + ) + return + + subscription_url = message.text.strip().rstrip("/") + + user_data = await state.get_data() + cluster_name = user_data.get("cluster_name") + server_name = user_data.get("server_name") + await state.update_data(subscription_url=subscription_url) + + text = ( + f"Введите inbound_id для сервера {server_name} в кластере {cluster_name}:\n\n" + "Это номер подключения vless в вашей панели 3x-ui. Обычно это 1 при чистой настройке по гайду.\n\n" + ) + + await message.answer( + text=text, + reply_markup=build_admin_back_kb("clusters"), + ) + await state.set_state(AdminClusterStates.waiting_for_inbound_id) + + +@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("clusters"), + ) + return + + user_data = await state.get_data() + cluster_name = user_data.get("cluster_name") + server_name = user_data.get("server_name") + api_url = user_data.get("api_url") + subscription_url = user_data.get("subscription_url") + + conn = await asyncpg.connect(DATABASE_URL) + await conn.execute( + """ + INSERT INTO servers (cluster_name, server_name, api_url, subscription_url, inbound_id) + VALUES ($1, $2, $3, $4, $5) + """, + cluster_name, + server_name, + api_url, + subscription_url, + inbound_id, + ) + await conn.close() + + await message.answer( + text=f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!", + reply_markup=build_admin_back_kb("clusters"), + ) + + await state.clear() + + +@router.callback_query(AdminClusterCallback.filter(F.action == "manage"), IsAdminFilter()) +async def handle_clusters_manage( + 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.edit_text( + text=f"🔧 Управление серверами для кластера {cluster_name}", + reply_markup=build_manage_cluster_kb(cluster_servers, cluster_name), + ) + + +@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 + + servers = await get_servers(session) + cluster_servers = servers.get(cluster_name, []) + + if not cluster_servers: + await callback_query.message.edit_text(text=f"Кластер '{cluster_name}' не содержит серверов.") + return + + text = ( + f"🖥️ Проверка доступности серверов для кластера {cluster_name}.\n\n" + "Это может занять до 1 минуты, пожалуйста, подождите..." + ) + + await callback_query.message.edit_text(text=text) + + 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) + + try: + await xui.login() + 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("clusters")) + + +@router.callback_query(AdminClusterCallback.filter(F.action == "backup"), IsAdminFilter()) +async def handle_clusters_backup( + 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, []) + + for server in cluster_servers: + xui = AsyncApi( + server["api_url"], + username=ADMIN_USERNAME, + password=ADMIN_PASSWORD, + logger=logger, + ) + await create_backup_and_send_to_admins(xui) + + text = ( + f"Бэкап для кластера {cluster_name} был успешно создан и отправлен администраторам!\n\n" + f"🔔 Бэкапы отправлены в боты панелей." + ) + + await callback_query.message.edit_text( + text=text, + reply_markup=build_admin_back_kb("clusters"), + ) + + +@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: + query_keys = """ + SELECT tg_id, client_id, email, expiry_time + FROM keys + WHERE server_id = $1 + """ + keys_to_sync = await session.fetch(query_keys, cluster_name) + + if not keys_to_sync: + await callback_query.message.answer( + text=f"❌ Нет ключей для синхронизации в кластере {cluster_name}.", + reply_markup=build_admin_back_kb("clusters"), + ) + return + + for key in keys_to_sync: + try: + await create_key_on_cluster( + cluster_name, + key["tg_id"], + key["client_id"], + key["email"], + key["expiry_time"], + ) + await asyncio.sleep(0.6) + except Exception as e: + logger.error(f"Ошибка при добавлении ключа {key['client_id']} в кластер {cluster_name}: {e}") + + await callback_query.message.answer( + text=f"✅ Ключи успешно синхронизированы для кластера {cluster_name}", + 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("clusters") + ) + + +@router.callback_query(AdminClusterCallback.filter(F.action == "change_domain"), IsAdminFilter()) +async def request_new_domain(callback_query: AdminClusterCallback, state: FSMContext): + """Запрашивает у администратора новый домен.""" + await state.set_state(AdminClusterStates.waiting_for_new_domain) + await callback_query.message.edit_text( + text="🌐 Введите новый домен (без https://):\nПример: pocomachodomen.ru", + ) + + +@router.message(AdminClusterStates.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..f453ef28 --- /dev/null +++ b/handlers/admin/clusters/keyboard.py @@ -0,0 +1,58 @@ +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() + + for cluster_name in servers: + builder.button( + text=f"⚙️ {cluster_name}", + callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack(), + ) + + builder.button(text="➕ Добавить кластер", callback_data=AdminClusterCallback(action="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=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/servers/keyboard.py b/handlers/admin/servers/keyboard.py index 762c697d..ebc5e390 100644 --- a/handlers/admin/servers/keyboard.py +++ b/handlers/admin/servers/keyboard.py @@ -1,69 +1,22 @@ from aiogram.filters.callback_data import CallbackData -from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from aiogram.types import InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder -from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn +from ..clusters.keyboard import AdminClusterCallback -class AdminServerEditorCallback(CallbackData, prefix="admin_servers"): +class AdminServerCallback(CallbackData, prefix="admin_server"): 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() + text="🗑️ Удалить", callback_data=AdminServerCallback(action="delete", data=server_name).pack() ) builder.button( - text="🔙 Назад", callback_data=AdminServerEditorCallback(action="clusters_manage", data=cluster_name).pack() + text="🔙 Назад", callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack() ) builder.adjust(1) return builder.as_markup() @@ -72,10 +25,10 @@ def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboar def build_delete_server_kb(server_name: str) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() builder.button( - text="✅ Да", callback_data=AdminServerEditorCallback(action="servers_delete_confirm", data=server_name).pack() + text="✅ Да", callback_data=AdminServerCallback(action="delete_confirm", data=server_name).pack() ) builder.button( - text="🔙 Назад", callback_data=AdminServerEditorCallback(action="servers_manage", data=server_name).pack() + 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 index 2b58393f..c5a07a41 100644 --- a/handlers/admin/servers/servers_handler.py +++ b/handlers/admin/servers/servers_handler.py @@ -1,311 +1,22 @@ -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 aiogram.types import CallbackQuery -from backup import create_backup_and_send_to_admins -from database import check_unique_server_name, delete_server, get_servers +from database import delete_server, get_servers from filters.admin import IsAdminFilter -from handlers.keys.key_utils import create_key_on_cluster -from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb from keyboard import ( - AdminServerEditorCallback, - build_clusters_editor_kb, + AdminServerCallback, build_delete_server_kb, - build_manage_cluster_kb, build_manage_server_kb, ) -from logger import logger - +from ..panel.keyboard import build_admin_back_kb router = Router() -class AdminServersEditor(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"), - IsAdminFilter(), -) -async def handle_servers(callback_query: CallbackQuery): - servers = await get_servers() - - text = ( - "🔧 Управление кластерами\n\n" - "📌 Здесь вы можете добавить новый кластер.\n\n" - "🌐 Кластеры — это пространство серверов, в пределах которого создается подписка.\n" - "💡 Если вы хотите выдавать по 1 серверу, то добавьте всего 1 сервер в кластер.\n\n" - "⚠️ Важно: Кластеры удаляются автоматически, если удалить все серверы внутри них.\n\n" - ) - - await callback_query.message.edit_text( - text=text, - reply_markup=build_clusters_editor_kb(servers), - ) - - -@router.callback_query( - AdminPanelCallback.filter(F.action == "clusters_add"), - IsAdminFilter(), -) -async def handle_clusters_add(callback_query: CallbackQuery, state: FSMContext): - text = ( - "🔧 Введите имя нового кластера:\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 state.set_state(AdminServersEditor.waiting_for_cluster_name) - - -@router.message(AdminServersEditor.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") - ) - return - - if len(message.text) > 12: - await message.answer( - text="❌ Имя кластера должно превышать 12 символов! Попробуйте снова.", - reply_markup=build_admin_back_kb("servers"), - ) - return - - cluster_name = message.text.strip() - await state.update_data(cluster_name=cluster_name) - - text = ( - f"Введите имя сервера для кластера {cluster_name}:\n\n" - "Рекомендуется указать локацию и номер сервера в имени.\n\n" - "Пример: de1, fra1, fi2" - ) - - await message.answer( - text=text, - reply_markup=build_admin_back_kb("servers"), - ) - - await state.set_state(AdminServersEditor.waiting_for_server_name) - - -@router.message(AdminServersEditor.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") - ) - return - - server_name = message.text.strip() - - if len(server_name) > 12: - await message.answer( - text="❌ Имя сервера не должно превышать 12 символов. Попробуйте снова.", - reply_markup=build_admin_back_kb("servers"), - ) - return - - user_data = await state.get_data() - cluster_name = user_data.get("cluster_name") - - if not await check_unique_server_name(server_name, session, cluster_name): - await message.answer( - text="❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя.", - reply_markup=build_admin_back_kb("servers"), - ) - return - - await state.update_data(server_name=server_name) - - text = ( - f"Введите API URL для сервера {server_name} в кластере {cluster_name}:\n\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"), - ) - - await state.set_state(AdminServersEditor.waiting_for_api_url) - - -@router.message(AdminServersEditor.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"), - ) - return - - api_url = message.text.strip().rstrip("/") - - user_data = await state.get_data() - cluster_name = user_data.get("cluster_name") - server_name = user_data.get("server_name") - await state.update_data(api_url=api_url) - - text = ( - f"Введите subscription_url для сервера {server_name} в кластере {cluster_name}:\n\n" - "Ссылку можно найти в панели 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"), - ) - - await state.set_state(AdminServersEditor.waiting_for_subscription_url) - - -@router.message(AdminServersEditor.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"), - ) - return - - subscription_url = message.text.strip().rstrip("/") - - user_data = await state.get_data() - cluster_name = user_data.get("cluster_name") - server_name = user_data.get("server_name") - await state.update_data(subscription_url=subscription_url) - - text = ( - f"Введите inbound_id для сервера {server_name} в кластере {cluster_name}:\n\n" - "Это номер подключения vless в вашей панели 3x-ui. Обычно это 1 при чистой настройке по гайду.\n\n" - ) - - await message.answer( - text=text, - reply_markup=build_admin_back_kb("servers"), - ) - await state.set_state(AdminServersEditor.waiting_for_inbound_id) - - -@router.message(AdminServersEditor.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"), - ) - return - - user_data = await state.get_data() - cluster_name = user_data.get("cluster_name") - server_name = user_data.get("server_name") - api_url = user_data.get("api_url") - subscription_url = user_data.get("subscription_url") - - conn = await asyncpg.connect(DATABASE_URL) - await conn.execute( - """ - INSERT INTO servers (cluster_name, server_name, api_url, subscription_url, inbound_id) - VALUES ($1, $2, $3, $4, $5) - """, - cluster_name, - server_name, - api_url, - subscription_url, - inbound_id, - ) - await conn.close() - - await message.answer( - text=f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!", - reply_markup=build_admin_back_kb("servers"), - ) - - await state.clear() - - -@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_manage"), IsAdminFilter()) -async def handle_clusters_manage( - callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any -): - cluster_name = callback_data.data - - servers = await get_servers(session) - cluster_servers = servers.get(cluster_name, []) - - await callback_query.message.edit_text( - 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 -): - cluster_name = callback_data.data - - servers = await get_servers(session) - cluster_servers = servers.get(cluster_name, []) - - if not cluster_servers: - await callback_query.message.edit_text(text=f"Кластер '{cluster_name}' не содержит серверов.") - return - - text = ( - f"🖥️ Проверка доступности серверов для кластера {cluster_name}.\n\n" - "Это может занять до 1 минуты, пожалуйста, подождите..." - ) - - await callback_query.message.edit_text(text=text) - - 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) - - try: - await xui.login() - 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")) - - -@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_manage"), IsAdminFilter()) -async def handle_servers_manage(callback_query: CallbackQuery, callback_data: AdminServerEditorCallback): +@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() @@ -333,8 +44,8 @@ async def handle_servers_manage(callback_query: CallbackQuery, callback_data: Ad 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): +@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( @@ -343,163 +54,14 @@ async def handle_servers_delete(callback_query: CallbackQuery, callback_data: Ad ) -@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 +@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("servers") + text=f"🗑️ Сервер {server_name} успешно удален.", reply_markup=build_admin_back_kb("clusters") ) - - -@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" - "Имя не должно превышать 12 символов!\n\n" - "Пример:\n" - "— de1 - для первого сервера в Германии\n" - "— swe2 - для второго сервера в Швеции\n" - "(если в каком-то кластере у вас уже есть сервер в Швеции)" - ) - - 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()) -async def handle_clusters_backup( - callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any -): - cluster_name = callback_data.data - - servers = await get_servers(session) - cluster_servers = servers.get(cluster_name, []) - - for server in cluster_servers: - xui = AsyncApi( - server["api_url"], - username=ADMIN_USERNAME, - password=ADMIN_PASSWORD, - logger=logger, - ) - await create_backup_and_send_to_admins(xui) - - text = ( - f"Бэкап для кластера {cluster_name} был успешно создан и отправлен администраторам!\n\n" - f"🔔 Бэкапы отправлены в боты панелей." - ) - - await callback_query.message.edit_text( - text=text, - reply_markup=build_admin_back_kb("servers"), - ) - - -@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_sync"), IsAdminFilter()) -async def handle_clusters_sync( - callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any -): - cluster_name = callback_data.data - - try: - query_keys = """ - SELECT tg_id, client_id, email, expiry_time - FROM keys - WHERE server_id = $1 - """ - keys_to_sync = await session.fetch(query_keys, cluster_name) - - if not keys_to_sync: - await callback_query.message.answer( - text=f"❌ Нет ключей для синхронизации в кластере {cluster_name}.", - reply_markup=build_admin_back_kb("servers"), - ) - return - - for key in keys_to_sync: - try: - await create_key_on_cluster( - cluster_name, - key["tg_id"], - key["client_id"], - key["email"], - key["expiry_time"], - ) - await asyncio.sleep(0.6) - except Exception as e: - logger.error(f"Ошибка при добавлении ключа {key['client_id']} в кластер {cluster_name}: {e}") - - await callback_query.message.answer( - text=f"✅ Ключи успешно синхронизированы для кластера {cluster_name}", - reply_markup=build_admin_back_kb("servers"), - ) - 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") - ) - - -@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() From 79fd3cde5def8c5997f2444416818501ceb1bc63 Mon Sep 17 00:00:00 2001 From: hteppl Date: Tue, 25 Mar 2025 17:21:05 +0300 Subject: [PATCH 08/16] Do not use clusters_handler for handling management actions --- handlers/admin/__init__.py | 2 + handlers/admin/clusters/clusters_handler.py | 50 ------------- handlers/admin/management/__init__.py | 3 + handlers/admin/management/keyboard.py | 16 ++++ .../admin/management/management_handler.py | 73 +++++++++++++++++++ handlers/admin/panel/keyboard.py | 12 --- handlers/admin/panel/panel_handler.py | 11 +-- 7 files changed, 95 insertions(+), 72 deletions(-) create mode 100644 handlers/admin/management/__init__.py create mode 100644 handlers/admin/management/keyboard.py create mode 100644 handlers/admin/management/management_handler.py diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py index f77eff4d..4ac02168 100644 --- a/handlers/admin/__init__.py +++ b/handlers/admin/__init__.py @@ -6,6 +6,7 @@ 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 @@ -17,6 +18,7 @@ router = Router(name="admins_main_router") router.include_routers( panel_router, + management_router, servers_router, clusters_router, users_router, diff --git a/handlers/admin/clusters/clusters_handler.py b/handlers/admin/clusters/clusters_handler.py index 31496a69..16253a0e 100644 --- a/handlers/admin/clusters/clusters_handler.py +++ b/handlers/admin/clusters/clusters_handler.py @@ -29,7 +29,6 @@ class AdminClusterStates(StatesGroup): waiting_for_inbound_id = State() waiting_for_server_name = State() waiting_for_subscription_url = State() - waiting_for_new_domain = State() @router.callback_query( @@ -370,52 +369,3 @@ async def handle_clusters_sync( await callback_query.message.answer( text=f"❌ Произошла ошибка при синхронизации: {e}", reply_markup=build_admin_back_kb("clusters") ) - - -@router.callback_query(AdminClusterCallback.filter(F.action == "change_domain"), IsAdminFilter()) -async def request_new_domain(callback_query: AdminClusterCallback, state: FSMContext): - """Запрашивает у администратора новый домен.""" - await state.set_state(AdminClusterStates.waiting_for_new_domain) - await callback_query.message.edit_text( - text="🌐 Введите новый домен (без https://):\nПример: pocomachodomen.ru", - ) - - -@router.message(AdminClusterStates.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/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/keyboard.py b/handlers/admin/panel/keyboard.py index f897e34f..4fab750c 100644 --- a/handlers/admin/panel/keyboard.py +++ b/handlers/admin/panel/keyboard.py @@ -33,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/panel/panel_handler.py b/handlers/admin/panel/panel_handler.py index 6e244516..328eb047 100644 --- a/handlers/admin/panel/panel_handler.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 keyboard 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(), - ) From 29ecb61ad220d39b86bd6aa8a7f7472dc32bc183 Mon Sep 17 00:00:00 2001 From: hteppl Date: Tue, 25 Mar 2025 17:27:25 +0300 Subject: [PATCH 09/16] Make clusters 2 in a row --- handlers/admin/clusters/clusters_handler.py | 13 ++++++------- handlers/admin/clusters/keyboard.py | 17 +++++++++++------ handlers/admin/servers/keyboard.py | 16 ++++------------ handlers/admin/servers/servers_handler.py | 2 +- 4 files changed, 22 insertions(+), 26 deletions(-) diff --git a/handlers/admin/clusters/clusters_handler.py b/handlers/admin/clusters/clusters_handler.py index 16253a0e..3f292d5d 100644 --- a/handlers/admin/clusters/clusters_handler.py +++ b/handlers/admin/clusters/clusters_handler.py @@ -15,7 +15,8 @@ from filters.admin import IsAdminFilter from handlers.keys.key_utils import create_key_on_cluster from keyboard import ( build_clusters_editor_kb, - build_manage_cluster_kb, AdminClusterCallback, + build_manage_cluster_kb, + AdminClusterCallback, ) from logger import logger from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb @@ -245,7 +246,7 @@ async def handle_inbound_id_input(message: Message, state: FSMContext): @router.callback_query(AdminClusterCallback.filter(F.action == "manage"), IsAdminFilter()) async def handle_clusters_manage( - callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any ): cluster_name = callback_data.data @@ -260,7 +261,7 @@ async def handle_clusters_manage( @router.callback_query(AdminClusterCallback.filter(F.action == "availability"), IsAdminFilter()) async def handle_cluster_availability( - callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any ): cluster_name = callback_data.data @@ -299,7 +300,7 @@ async def handle_cluster_availability( @router.callback_query(AdminClusterCallback.filter(F.action == "backup"), IsAdminFilter()) async def handle_clusters_backup( - callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any ): cluster_name = callback_data.data @@ -327,9 +328,7 @@ async def handle_clusters_backup( @router.callback_query(AdminClusterCallback.filter(F.action == "sync"), IsAdminFilter()) -async def handle_clusters_sync( - callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any -): +async def handle_clusters_sync(callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any): cluster_name = callback_data.data try: diff --git a/handlers/admin/clusters/keyboard.py b/handlers/admin/clusters/keyboard.py index f453ef28..bb3d61b2 100644 --- a/handlers/admin/clusters/keyboard.py +++ b/handlers/admin/clusters/keyboard.py @@ -14,15 +14,20 @@ class AdminClusterCallback(CallbackData, prefix="admin_cluster"): def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - for cluster_name in servers: - builder.button( - text=f"⚙️ {cluster_name}", - callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack(), - ) + 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()) - builder.adjust(1) return builder.as_markup() diff --git a/handlers/admin/servers/keyboard.py b/handlers/admin/servers/keyboard.py index ebc5e390..9077ff4d 100644 --- a/handlers/admin/servers/keyboard.py +++ b/handlers/admin/servers/keyboard.py @@ -12,23 +12,15 @@ class AdminServerCallback(CallbackData, prefix="admin_server"): 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.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.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 index c5a07a41..c5199bd6 100644 --- a/handlers/admin/servers/servers_handler.py +++ b/handlers/admin/servers/servers_handler.py @@ -56,7 +56,7 @@ async def handle_server_delete(callback_query: CallbackQuery, callback_data: Adm @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 + callback_query: types.CallbackQuery, callback_data: AdminServerCallback, session: Any ): server_name = callback_data.data From ff591fc3872df71a1c7579948e076990b7a5cc78 Mon Sep 17 00:00:00 2001 From: hteppl Date: Tue, 25 Mar 2025 17:42:53 +0300 Subject: [PATCH 10/16] Optimize and fix imports --- backup.py | 4 +--- bot.py | 3 +-- client.py | 1 - database.py | 2 -- filters/admin.py | 1 + handlers/__init__.py | 1 - handlers/admin/backups/backups_handler.py | 1 - handlers/admin/bans/bans_handler.py | 1 - handlers/admin/clusters/keyboard.py | 2 +- handlers/admin/coupons/coupons_handler.py | 5 ++--- handlers/admin/restart/restart_handler.py | 1 - handlers/admin/sender/sender_handler.py | 3 +-- handlers/admin/stats/stats_handler.py | 4 +--- handlers/admin/users/keyboard.py | 2 +- handlers/admin/users/users_handler.py | 11 ++++------- handlers/captcha.py | 4 +--- handlers/donate.py | 3 +-- handlers/instructions/__init__.py | 1 - handlers/instructions/instructions.py | 6 ++---- handlers/keys/__init__.py | 1 - handlers/keys/key_management.py | 13 +++++-------- handlers/keys/key_utils.py | 5 ++--- handlers/keys/keys.py | 19 ++++++------------- handlers/keys/subscriptions.py | 5 +---- handlers/notifications/__init__.py | 1 - .../notifications/general_notifications.py | 8 +++++++- handlers/notifications/notify_utils.py | 1 - .../notifications/special_notifications.py | 16 ++++------------ handlers/pay.py | 2 +- handlers/payments/__init__.py | 2 +- handlers/payments/robokassa_pay.py | 6 +++--- handlers/profile.py | 9 +++------ handlers/start.py | 11 ++++------- handlers/utils.py | 3 +-- logger.py | 2 -- middlewares/__init__.py | 1 - middlewares/admin.py | 1 + middlewares/session.py | 2 +- servers.py | 8 +++----- utils/csv_export.py | 2 +- 40 files changed, 61 insertions(+), 113 deletions(-) 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/backups/backups_handler.py b/handlers/admin/backups/backups_handler.py index 35d9c0e8..b466d255 100644 --- a/handlers/admin/backups/backups_handler.py +++ b/handlers/admin/backups/backups_handler.py @@ -5,7 +5,6 @@ from backup import backup_database from filters.admin import IsAdminFilter from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb - router = Router() diff --git a/handlers/admin/bans/bans_handler.py b/handlers/admin/bans/bans_handler.py index 28f44213..b00d51bc 100644 --- a/handlers/admin/bans/bans_handler.py +++ b/handlers/admin/bans/bans_handler.py @@ -8,7 +8,6 @@ from filters.admin import IsAdminFilter from keyboard import build_bans_kb from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb - router = Router() diff --git a/handlers/admin/clusters/keyboard.py b/handlers/admin/clusters/keyboard.py index bb3d61b2..a86494a0 100644 --- a/handlers/admin/clusters/keyboard.py +++ b/handlers/admin/clusters/keyboard.py @@ -17,7 +17,7 @@ def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup: 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]: + for cluster_name in cluster_names[i : i + 2]: row_buttons.append( InlineKeyboardButton( text=f"⚙️ {cluster_name}", diff --git a/handlers/admin/coupons/coupons_handler.py b/handlers/admin/coupons/coupons_handler.py index 1c7bbe92..09d02c03 100644 --- a/handlers/admin/coupons/coupons_handler.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 keyboard import AdminCouponDeleteCallback, build_coupons_kb, build_coupons_list_kb -from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb from logger import logger - +from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb router = Router() diff --git a/handlers/admin/restart/restart_handler.py b/handlers/admin/restart/restart_handler.py index 12b4c013..b477f776 100644 --- a/handlers/admin/restart/restart_handler.py +++ b/handlers/admin/restart/restart_handler.py @@ -6,7 +6,6 @@ from aiogram.types import CallbackQuery from filters.admin import IsAdminFilter from handlers.admin.panel.keyboard import AdminPanelCallback, build_admin_back_kb, build_restart_kb - router = Router() diff --git a/handlers/admin/sender/sender_handler.py b/handlers/admin/sender/sender_handler.py index bfe33492..652b799f 100644 --- a/handlers/admin/sender/sender_handler.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 ..panel.keyboard import AdminPanelCallback, build_admin_back_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() diff --git a/handlers/admin/stats/stats_handler.py b/handlers/admin/stats/stats_handler.py index 92e68c33..9707beb7 100644 --- a/handlers/admin/stats/stats_handler.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 ..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 - +from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb router = Router() diff --git a/handlers/admin/users/keyboard.py b/handlers/admin/users/keyboard.py index 9697c526..a01ba28e 100644 --- a/handlers/admin/users/keyboard.py +++ b/handlers/admin/users/keyboard.py @@ -3,8 +3,8 @@ 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 config import RENEWAL_PRICES from ..panel.keyboard import build_admin_back_btn diff --git a/handlers/admin/users/users_handler.py b/handlers/admin/users/users_handler.py index db48c12e..c2dad526 100644 --- a/handlers/admin/users/users_handler.py +++ b/handlers/admin/users/users_handler.py @@ -1,19 +1,16 @@ import asyncio - from datetime import datetime, timedelta, timezone from typing import Any import pytz - from aiogram import F, Router, types from aiogram.exceptions import TelegramBadRequest from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, Message from aiogram.utils.keyboard import InlineKeyboardBuilder -from config import TOTAL_GB -from utils.csv_export import export_referrals_csv +from config import TOTAL_GB from database import ( delete_key, delete_user_data, @@ -27,8 +24,10 @@ 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 logger import logger +from utils.csv_export import export_referrals_csv from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb, build_admin_back_btn -from keyboards.admin.users_kb import ( +from ..users.keyboard import ( AdminUserEditorCallback, AdminUserKeyEditorCallback, build_editor_kb, @@ -42,8 +41,6 @@ from keyboards.admin.users_kb import ( build_users_key_expiry_kb, build_users_key_show_kb, ) -from logger import logger - MOSCOW_TZ = pytz.timezone("Europe/Moscow") diff --git a/handlers/captcha.py b/handlers/captcha.py index 8f294895..5ae3790f 100644 --- a/handlers/captcha.py +++ b/handlers/captcha.py @@ -1,19 +1,17 @@ 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 9debef3e..961ebad0 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 ( DISCOUNTS, key_message_success, @@ -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 d9540275..3a71e8cf 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -1,12 +1,11 @@ import asyncio - from typing import Any -from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, DATABASE_URL -from py3xui import AsyncApi import asyncpg +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 28eee02a..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, @@ -28,8 +29,6 @@ from config import ( USE_NEW_PAYMENT_FLOW, TOGGLE_CLIENT, ) - -from bot import bot from database import ( check_server_name_by_cluster, create_temporary_data, @@ -57,7 +56,6 @@ from handlers.keys.key_utils import ( 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 ( DISCOUNTS, KEY_NOT_FOUND_MSG, @@ -78,7 +76,6 @@ from handlers.texts import ( 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() @@ -354,7 +351,7 @@ async def process_callback_unfreeze_subscription_confirm(callback_query: Callbac email=email, client_id=client_id, new_expiry_time=new_expiry_time, - total_gb=TOTAL_GB + total_gb=TOTAL_GB, ) text_ok = SUBSCRIPTION_UNFROZEN_MSG builder = InlineKeyboardBuilder() @@ -549,14 +546,10 @@ async def process_callback_delete_key(callback_query: CallbackQuery): if callback_query.message.caption: await callback_query.message.edit_caption( - caption=DELETE_KEY_CONFIRM_MSG, - reply_markup=confirmation_keyboard + caption=DELETE_KEY_CONFIRM_MSG, reply_markup=confirmation_keyboard ) else: - await callback_query.message.edit_text( - text=DELETE_KEY_CONFIRM_MSG, - reply_markup=confirmation_keyboard - ) + await callback_query.message.edit_text(text=DELETE_KEY_CONFIRM_MSG, reply_markup=confirmation_keyboard) except Exception as e: logger.error(f"Ошибка при обработке запроса на удаление ключа {client_id}: {e}") diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index d789359a..ccd75c95 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 d11f6695..f5669ae1 100644 --- a/handlers/notifications/general_notifications.py +++ b/handlers/notifications/general_notifications.py @@ -1,9 +1,10 @@ import asyncio from datetime import datetime, timedelta + import asyncpg import pytz - from aiogram import Bot, Router + from config import ( DATABASE_URL, NOTIFICATION_TIME, @@ -48,6 +49,7 @@ router = Router() moscow_tz = pytz.timezone("Europe/Moscow") + async def periodic_notifications(bot: Bot): """ Обработчик, который: @@ -98,6 +100,7 @@ async def periodic_notifications(bot: Bot): await asyncio.sleep(NOTIFICATION_TIME) + async def notify_24h_keys(bot: Bot, conn: asyncpg.Connection, current_time: int, threshold_time_24h: int, keys: list): logger.info("Начало проверки подписок, истекающих через 24 часа.") @@ -149,6 +152,7 @@ async def notify_24h_keys(bot: Bot, conn: asyncpg.Connection, current_time: int, logger.info("✅ Обработка всех уведомлений за 24 часа завершена.") await asyncio.sleep(1) + async def notify_10h_keys(bot: Bot, conn: asyncpg.Connection, current_time: int, threshold_time_10h: int, keys: list): """ Отправляет уведомления пользователям о том, что их подписка истекает через 10 часов. @@ -208,6 +212,7 @@ async def notify_10h_keys(bot: Bot, conn: asyncpg.Connection, current_time: int, logger.info("✅ Обработка всех уведомлений за 10 часов завершена.") await asyncio.sleep(1) + async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: int, keys: list): """ Обрабатывает истекшие ключи, проверяя продление или удаление. @@ -319,6 +324,7 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: logger.info("✅ Обработка истекших ключей завершена.") await asyncio.sleep(1) + async def process_auto_renew_or_notify( bot, conn, key: dict, notification_id: str, renewal_period_months: int, standard_photo: str, standard_caption: str ): 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 9ca81461..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.texts import TRIAL_INACTIVE_FIRST_MSG, TRIAL_INACTIVE_BONUS_MSG, ZERO_TRAFFIC_MSG from handlers.keys.key_utils import get_user_traffic +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") @@ -84,16 +81,11 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): if trial_extended: total_days = NOTIFY_EXTRA_DAYS + TRIAL_TIME message = TRIAL_INACTIVE_BONUS_MSG.format( - display_name=display_name, - NOTIFY_EXTRA_DAYS=NOTIFY_EXTRA_DAYS, - total_days=total_days + display_name=display_name, NOTIFY_EXTRA_DAYS=NOTIFY_EXTRA_DAYS, total_days=total_days ) await conn.execute("UPDATE connections SET trial = -1 WHERE tg_id = $1", tg_id) else: - message = TRIAL_INACTIVE_FIRST_MSG.format( - display_name=display_name, - TRIAL_TIME=TRIAL_TIME - ) + message = TRIAL_INACTIVE_FIRST_MSG.format(display_name=display_name, TRIAL_TIME=TRIAL_TIME) try: await bot.send_message(tg_id, message, reply_markup=keyboard) diff --git a/handlers/pay.py b/handlers/pay.py index 036df442..9d36f57a 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, @@ -12,7 +13,6 @@ from config import ( 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 043d3e52..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,12 +38,11 @@ from handlers.buttons.profile import ( MY_SUBS, PAYMENT, ) -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 10a8667e..290c12c4 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -10,6 +10,8 @@ from aiogram.types import ( Message, ) from aiogram.utils.keyboard import InlineKeyboardBuilder + +from bot import bot from config import ( CAPTCHA_ENABLE, CHANNEL_EXISTS, @@ -19,8 +21,6 @@ from config import ( DONATIONS_ENABLE, SUPPORT_CHAT_URL, ) - -from bot import bot from database import ( add_connection, add_referral, @@ -43,12 +43,10 @@ from handlers.texts import ( NEW_REFERRAL_NOTIFICATION, COUPON_SUCCESS_MSG, ) -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() @@ -274,8 +272,7 @@ async def process_start_logic( await message.answer(REFERRAL_SUCCESS_MSG.format(referrer_tg_id=referrer_tg_id)) try: await bot.send_message( - referrer_tg_id, - NEW_REFERRAL_NOTIFICATION.format(referred_id=message.chat.id) + referrer_tg_id, NEW_REFERRAL_NOTIFICATION.format(referred_id=message.chat.id) ) logger.info( f"Уведомление отправлено пользователю {referrer_tg_id} о новом реферале {message.chat.id}" diff --git a/handlers/utils.py b/handlers/utils.py index b08c2475..739334c9 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/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 179249c7..6023c1ae 100644 --- a/utils/csv_export.py +++ b/utils/csv_export.py @@ -1,6 +1,6 @@ +import csv from io import StringIO from typing import Any -import csv from aiogram.types import BufferedInputFile From c7915a45b898c48e4abdfaeaa2c6d10f9610408d Mon Sep 17 00:00:00 2001 From: hteppl Date: Wed, 26 Mar 2025 00:16:14 +0300 Subject: [PATCH 11/16] Fix some back buttons --- handlers/admin/clusters/keyboard.py | 2 +- handlers/admin/panel/keyboard.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/handlers/admin/clusters/keyboard.py b/handlers/admin/clusters/keyboard.py index a86494a0..c1093a0a 100644 --- a/handlers/admin/clusters/keyboard.py +++ b/handlers/admin/clusters/keyboard.py @@ -58,6 +58,6 @@ def build_manage_cluster_kb(cluster_servers, cluster_name) -> InlineKeyboardMark text="💾 Создать бэкап кластера", callback_data=AdminClusterCallback(action="backup", data=cluster_name).pack(), ) - builder.row(build_admin_back_btn("servers")) + builder.row(build_admin_back_btn("clusters")) builder.adjust(1, 1, 1, 1, 1, 2, 1) return builder.as_markup() diff --git a/handlers/admin/panel/keyboard.py b/handlers/admin/panel/keyboard.py index 4fab750c..37fe725a 100644 --- a/handlers/admin/panel/keyboard.py +++ b/handlers/admin/panel/keyboard.py @@ -20,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()) From 70293fe387250f8894008d0204e27c35ca9ed447 Mon Sep 17 00:00:00 2001 From: hteppl Date: Wed, 26 Mar 2025 00:19:24 +0300 Subject: [PATCH 12/16] Fix back button in sender + add broadcast start text --- handlers/admin/sender/sender_handler.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/handlers/admin/sender/sender_handler.py b/handlers/admin/sender/sender_handler.py index 652b799f..200ae9cd 100644 --- a/handlers/admin/sender/sender_handler.py +++ b/handlers/admin/sender/sender_handler.py @@ -34,7 +34,7 @@ async def handle_sender(callback_query: CallbackQuery): IsAdminFilter(), ) async def handle_sender_callback_text( - callback_query: CallbackQuery, callback_data: AdminSenderCallback, state: FSMContext + callback_query: CallbackQuery, callback_data: AdminSenderCallback, state: FSMContext ): await callback_query.message.edit_text( text="✍️ Введите текст сообщения для рассылки:", @@ -114,6 +114,13 @@ async def handle_message_input(message: Message, state: FSMContext, session: Any total_users = len(tg_ids) success_count = 0 + text = ( + f"📤 Рассылка начата!\n\n" + f"👥 Всего пользователей: {total_users}\n" + ) + + await message.answer(text=text) + for record in tg_ids: tg_id = record["tg_id"] try: @@ -135,6 +142,5 @@ async def handle_message_input(message: Message, state: FSMContext, session: Any 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() From 46f71a7a6e9f7fc83f702e86550cc49b1f669480 Mon Sep 17 00:00:00 2001 From: hteppl Date: Wed, 26 Mar 2025 01:11:54 +0300 Subject: [PATCH 13/16] Update init routing for handlers --- handlers/admin/backups/__init__.py | 2 +- handlers/admin/bans/__init__.py | 2 +- handlers/admin/clusters/__init__.py | 2 +- handlers/admin/coupons/__init__.py | 2 +- handlers/admin/management/__init__.py | 2 +- handlers/admin/panel/__init__.py | 2 +- handlers/admin/restart/__init__.py | 2 +- handlers/admin/sender/__init__.py | 2 +- handlers/admin/servers/__init__.py | 2 +- handlers/admin/stats/__init__.py | 2 +- handlers/admin/users/__init__.py | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/handlers/admin/backups/__init__.py b/handlers/admin/backups/__init__.py index 5160dafc..3a2f8d4a 100644 --- a/handlers/admin/backups/__init__.py +++ b/handlers/admin/backups/__init__.py @@ -1,3 +1,3 @@ __all__ = ("router",) -from backups_handler import router +from .backups_handler import router diff --git a/handlers/admin/bans/__init__.py b/handlers/admin/bans/__init__.py index d03ed5a7..2fc0494c 100644 --- a/handlers/admin/bans/__init__.py +++ b/handlers/admin/bans/__init__.py @@ -1,3 +1,3 @@ __all__ = ("router",) -from bans_handler import router +from .bans_handler import router diff --git a/handlers/admin/clusters/__init__.py b/handlers/admin/clusters/__init__.py index 58b5c5e4..2a213492 100644 --- a/handlers/admin/clusters/__init__.py +++ b/handlers/admin/clusters/__init__.py @@ -1,3 +1,3 @@ __all__ = ("router",) -from clusters_handler import router +from .clusters_handler import router diff --git a/handlers/admin/coupons/__init__.py b/handlers/admin/coupons/__init__.py index 1948a0fd..1c00dee2 100644 --- a/handlers/admin/coupons/__init__.py +++ b/handlers/admin/coupons/__init__.py @@ -1,3 +1,3 @@ __all__ = ("router",) -from coupons_handler import router +from .coupons_handler import router diff --git a/handlers/admin/management/__init__.py b/handlers/admin/management/__init__.py index 1a6a84ae..16ca5b24 100644 --- a/handlers/admin/management/__init__.py +++ b/handlers/admin/management/__init__.py @@ -1,3 +1,3 @@ __all__ = ("router",) -from management_handler import router +from .management_handler import router diff --git a/handlers/admin/panel/__init__.py b/handlers/admin/panel/__init__.py index 05a477e6..7bdbaaff 100644 --- a/handlers/admin/panel/__init__.py +++ b/handlers/admin/panel/__init__.py @@ -1,3 +1,3 @@ __all__ = ("router",) -from panel_handler import router +from .panel_handler import router diff --git a/handlers/admin/restart/__init__.py b/handlers/admin/restart/__init__.py index 7ac5de76..39fd65c6 100644 --- a/handlers/admin/restart/__init__.py +++ b/handlers/admin/restart/__init__.py @@ -1,3 +1,3 @@ __all__ = ("router",) -from restart_handler import router +from .restart_handler import router diff --git a/handlers/admin/sender/__init__.py b/handlers/admin/sender/__init__.py index 169939bc..0172e278 100644 --- a/handlers/admin/sender/__init__.py +++ b/handlers/admin/sender/__init__.py @@ -1,3 +1,3 @@ __all__ = ("router",) -from sender_handler import router +from .sender_handler import router diff --git a/handlers/admin/servers/__init__.py b/handlers/admin/servers/__init__.py index c4319df5..e0d03891 100644 --- a/handlers/admin/servers/__init__.py +++ b/handlers/admin/servers/__init__.py @@ -1,3 +1,3 @@ __all__ = ("router",) -from servers_handler import router +from .servers_handler import router diff --git a/handlers/admin/stats/__init__.py b/handlers/admin/stats/__init__.py index cd21d017..8f491060 100644 --- a/handlers/admin/stats/__init__.py +++ b/handlers/admin/stats/__init__.py @@ -1,3 +1,3 @@ __all__ = ("router",) -from stats_handler import router +from .stats_handler import router diff --git a/handlers/admin/users/__init__.py b/handlers/admin/users/__init__.py index 139f10e5..3dabbb0d 100644 --- a/handlers/admin/users/__init__.py +++ b/handlers/admin/users/__init__.py @@ -1,3 +1,3 @@ __all__ = ("router",) -from users_handler import router +from .users_handler import router From 11236ac64eddf61d5c748b39f14baa0a713a8047 Mon Sep 17 00:00:00 2001 From: hteppl Date: Wed, 26 Mar 2025 01:58:59 +0300 Subject: [PATCH 14/16] Implement separate sync logic --- handlers/admin/clusters/clusters_handler.py | 107 +++++++++++++++++--- handlers/admin/clusters/keyboard.py | 77 ++++++++++---- 2 files changed, 150 insertions(+), 34 deletions(-) diff --git a/handlers/admin/clusters/clusters_handler.py b/handlers/admin/clusters/clusters_handler.py index 3f292d5d..da95696b 100644 --- a/handlers/admin/clusters/clusters_handler.py +++ b/handlers/admin/clusters/clusters_handler.py @@ -12,13 +12,13 @@ from backup import create_backup_and_send_to_admins from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL from database import check_unique_server_name, get_servers from filters.admin import IsAdminFilter -from handlers.keys.key_utils import create_key_on_cluster -from keyboard import ( +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, + AdminClusterCallback, build_sync_cluster_kb, ) -from logger import logger from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb router = Router() @@ -246,7 +246,7 @@ async def handle_inbound_id_input(message: Message, state: FSMContext): @router.callback_query(AdminClusterCallback.filter(F.action == "manage"), IsAdminFilter()) async def handle_clusters_manage( - callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any ): cluster_name = callback_data.data @@ -254,14 +254,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(AdminClusterCallback.filter(F.action == "availability"), IsAdminFilter()) async def handle_cluster_availability( - callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any ): cluster_name = callback_data.data @@ -280,7 +280,7 @@ async def handle_cluster_availability( await callback_query.message.edit_text(text=text) total_online_users = 0 - result_text = f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n" + 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) @@ -289,18 +289,18 @@ async def handle_cluster_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("clusters")) @router.callback_query(AdminClusterCallback.filter(F.action == "backup"), IsAdminFilter()) async def handle_clusters_backup( - callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any ): cluster_name = callback_data.data @@ -328,7 +328,79 @@ async def handle_clusters_backup( @router.callback_query(AdminClusterCallback.filter(F.action == "sync"), IsAdminFilter()) -async def handle_clusters_sync(callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any): +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" + f"🔑 Количество ключей: {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: @@ -346,6 +418,15 @@ async def handle_clusters_sync(callback_query: types.CallbackQuery, callback_dat ) return + text = ( + f"🔄 Синхронизация кластера {cluster_name}\n\n" + f"🔑 Количество ключей: {len(keys_to_sync)}" + ) + + await callback_query.message.answer( + text=text, + ) + for key in keys_to_sync: try: await create_key_on_cluster( diff --git a/handlers/admin/clusters/keyboard.py b/handlers/admin/clusters/keyboard.py index c1093a0a..54bf5e4e 100644 --- a/handlers/admin/clusters/keyboard.py +++ b/handlers/admin/clusters/keyboard.py @@ -8,7 +8,7 @@ from ..servers.keyboard import AdminServerCallback class AdminClusterCallback(CallbackData, prefix="admin_cluster"): action: str - data: str + data: str = None def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup: @@ -16,34 +16,43 @@ def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup: 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(*[ + InlineKeyboardButton( + text=f"⚙️ {name}", + callback_data=AdminClusterCallback(action="manage", data=name).pack(), ) - builder.row(*row_buttons) + for name in cluster_names[i: i + 2] + ]) + + builder.row( + InlineKeyboardButton( + text="➕ Добавить кластер", callback_data=AdminClusterCallback(action="add").pack() + ) + ) - 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: +def build_manage_cluster_kb(cluster_servers: list, cluster_name: str) -> 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.row( + InlineKeyboardButton( + 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=AdminServerCallback(action="add", data=cluster_name).pack(), + ) ) + builder.row( InlineKeyboardButton( text="🌐 Доступность", @@ -54,10 +63,36 @@ def build_manage_cluster_kb(cluster_servers, cluster_name) -> InlineKeyboardMark callback_data=AdminClusterCallback(action="sync", data=cluster_name).pack(), ), ) - builder.button( - text="💾 Создать бэкап кластера", - callback_data=AdminClusterCallback(action="backup", data=cluster_name).pack(), + + builder.row( + InlineKeyboardButton( + text="💾 Создать бэкап", + callback_data=AdminClusterCallback(action="backup", data=cluster_name).pack(), + ) ) + builder.row(build_admin_back_btn("clusters")) - builder.adjust(1, 1, 1, 1, 1, 2, 1) + 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() From 372933e230a63a7fe7ab61a8eff4f9ebbe261e87 Mon Sep 17 00:00:00 2001 From: hteppl Date: Wed, 26 Mar 2025 01:59:52 +0300 Subject: [PATCH 15/16] Implement untrial type for sender --- handlers/admin/sender/keyboard.py | 28 ++++++++++++++++++------- handlers/admin/sender/sender_handler.py | 13 ++++++------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/handlers/admin/sender/keyboard.py b/handlers/admin/sender/keyboard.py index 0a4991eb..97e92710 100644 --- a/handlers/admin/sender/keyboard.py +++ b/handlers/admin/sender/keyboard.py @@ -1,5 +1,5 @@ from aiogram.filters.callback_data import CallbackData -from aiogram.types import InlineKeyboardMarkup +from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from ..panel.keyboard import build_admin_back_btn @@ -12,21 +12,35 @@ class AdminSenderCallback(CallbackData, prefix="admin_sender"): def build_sender_kb() -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() - builder.button(text="👥 Все пользователи", callback_data=AdminSenderCallback(type="all").pack()) - builder.button(text="✅ Пользователи с подпиской", callback_data=AdminSenderCallback(type="subscribed").pack()) - builder.button(text="❌ Пользователи без подписки", callback_data=AdminSenderCallback(type="unsubscribed").pack()) - builder.button(text="📢 Пользователи кластера", callback_data=AdminSenderCallback(type="cluster-select").pack()) + + 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()) - 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.adjust(2) builder.row(build_admin_back_btn()) - builder.adjust(1) + return builder.as_markup() diff --git a/handlers/admin/sender/sender_handler.py b/handlers/admin/sender/sender_handler.py index 200ae9cd..7891a46e 100644 --- a/handlers/admin/sender/sender_handler.py +++ b/handlers/admin/sender/sender_handler.py @@ -7,7 +7,7 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, Message from filters.admin import IsAdminFilter -from keyboard 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 @@ -34,7 +34,7 @@ async def handle_sender(callback_query: CallbackQuery): IsAdminFilter(), ) async def handle_sender_callback_text( - callback_query: CallbackQuery, callback_data: AdminSenderCallback, state: FSMContext + callback_query: CallbackQuery, callback_data: AdminSenderCallback, state: FSMContext ): await callback_query.message.edit_text( text="✍️ Введите текст сообщения для рассылки:", @@ -96,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( @@ -114,10 +116,7 @@ async def handle_message_input(message: Message, state: FSMContext, session: Any total_users = len(tg_ids) success_count = 0 - text = ( - f"📤 Рассылка начата!\n\n" - f"👥 Всего пользователей: {total_users}\n" - ) + text = f"📤 Рассылка начата!\n👥 Количество получателей: {total_users}" await message.answer(text=text) @@ -137,7 +136,7 @@ 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}" ) From f2a47de5da18997d4a4906c7e1189e34a573864b Mon Sep 17 00:00:00 2001 From: hteppl Date: Wed, 26 Mar 2025 02:00:28 +0300 Subject: [PATCH 16/16] Fix packages imports --- handlers/admin/bans/bans_handler.py | 2 +- handlers/admin/clusters/clusters_handler.py | 21 +++++++------------ handlers/admin/clusters/keyboard.py | 6 ++---- handlers/admin/coupons/coupons_handler.py | 2 +- .../admin/management/management_handler.py | 4 ++-- handlers/admin/panel/panel_handler.py | 2 +- handlers/admin/restart/restart_handler.py | 2 +- handlers/admin/sender/keyboard.py | 4 +--- handlers/admin/servers/keyboard.py | 4 ++-- handlers/admin/servers/servers_handler.py | 2 +- handlers/admin/stats/stats_handler.py | 2 +- handlers/admin/users/users_handler.py | 2 +- 12 files changed, 22 insertions(+), 31 deletions(-) diff --git a/handlers/admin/bans/bans_handler.py b/handlers/admin/bans/bans_handler.py index b00d51bc..360ce4ac 100644 --- a/handlers/admin/bans/bans_handler.py +++ b/handlers/admin/bans/bans_handler.py @@ -5,7 +5,7 @@ from aiogram.types import BufferedInputFile, CallbackQuery from database import delete_user_data from filters.admin import IsAdminFilter -from keyboard import build_bans_kb +from .keyboard import build_bans_kb from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb router = Router() diff --git a/handlers/admin/clusters/clusters_handler.py b/handlers/admin/clusters/clusters_handler.py index da95696b..342d0423 100644 --- a/handlers/admin/clusters/clusters_handler.py +++ b/handlers/admin/clusters/clusters_handler.py @@ -17,7 +17,8 @@ from logger import logger from .keyboard import ( build_clusters_editor_kb, build_manage_cluster_kb, - AdminClusterCallback, build_sync_cluster_kb, + AdminClusterCallback, + build_sync_cluster_kb, ) from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb @@ -246,7 +247,7 @@ async def handle_inbound_id_input(message: Message, state: FSMContext): @router.callback_query(AdminClusterCallback.filter(F.action == "manage"), IsAdminFilter()) async def handle_clusters_manage( - callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any ): cluster_name = callback_data.data @@ -261,7 +262,7 @@ async def handle_clusters_manage( @router.callback_query(AdminClusterCallback.filter(F.action == "availability"), IsAdminFilter()) async def handle_cluster_availability( - callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any ): cluster_name = callback_data.data @@ -300,7 +301,7 @@ async def handle_cluster_availability( @router.callback_query(AdminClusterCallback.filter(F.action == "backup"), IsAdminFilter()) async def handle_clusters_backup( - callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminClusterCallback, session: Any ): cluster_name = callback_data.data @@ -360,10 +361,7 @@ async def handle_sync_server(callback_query: types.CallbackQuery, callback_data: ) return - text = ( - f"🔄 Синхронизация сервера {server_name}\n\n" - f"🔑 Количество ключей: {len(keys_to_sync)}" - ) + text = f"🔄 Синхронизация сервера {server_name}\n\n🔑 Количество ключей: {len(keys_to_sync)}" await callback_query.message.answer( text=text, @@ -382,7 +380,7 @@ async def handle_sync_server(callback_query: types.CallbackQuery, callback_data: key["client_id"], key["email"], key["expiry_time"], - semaphore + semaphore, ) await asyncio.sleep(0.6) except Exception as e: @@ -418,10 +416,7 @@ async def handle_sync_cluster(callback_query: types.CallbackQuery, callback_data ) return - text = ( - f"🔄 Синхронизация кластера {cluster_name}\n\n" - f"🔑 Количество ключей: {len(keys_to_sync)}" - ) + text = f"🔄 Синхронизация кластера {cluster_name}\n\n🔑 Количество ключей: {len(keys_to_sync)}" await callback_query.message.answer( text=text, diff --git a/handlers/admin/clusters/keyboard.py b/handlers/admin/clusters/keyboard.py index 54bf5e4e..d761cd06 100644 --- a/handlers/admin/clusters/keyboard.py +++ b/handlers/admin/clusters/keyboard.py @@ -21,13 +21,11 @@ def build_clusters_editor_kb(servers: dict) -> InlineKeyboardMarkup: text=f"⚙️ {name}", callback_data=AdminClusterCallback(action="manage", data=name).pack(), ) - for name in cluster_names[i: i + 2] + for name in cluster_names[i : i + 2] ]) builder.row( - InlineKeyboardButton( - text="➕ Добавить кластер", callback_data=AdminClusterCallback(action="add").pack() - ) + InlineKeyboardButton(text="➕ Добавить кластер", callback_data=AdminClusterCallback(action="add").pack()) ) builder.row(build_admin_back_btn()) diff --git a/handlers/admin/coupons/coupons_handler.py b/handlers/admin/coupons/coupons_handler.py index 09d02c03..68605fa8 100644 --- a/handlers/admin/coupons/coupons_handler.py +++ b/handlers/admin/coupons/coupons_handler.py @@ -8,7 +8,7 @@ from aiogram.types import CallbackQuery, Message from config import USERNAME_BOT from database import create_coupon, delete_coupon, get_all_coupons from filters.admin import IsAdminFilter -from keyboard import AdminCouponDeleteCallback, build_coupons_kb, build_coupons_list_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 diff --git a/handlers/admin/management/management_handler.py b/handlers/admin/management/management_handler.py index cf3629e8..0e8a5636 100644 --- a/handlers/admin/management/management_handler.py +++ b/handlers/admin/management/management_handler.py @@ -5,7 +5,7 @@ from aiogram.types import CallbackQuery, Message from asyncpg import Connection from filters.admin import IsAdminFilter -from keyboard import AdminPanelCallback, build_management_kb +from .keyboard import AdminPanelCallback, build_management_kb from logger import logger from ..panel.keyboard import build_admin_back_kb @@ -25,7 +25,7 @@ async def handle_management(callback_query: CallbackQuery): @router.callback_query(AdminPanelCallback.filter(F.action == "change_domain"), IsAdminFilter()) -async def request_new_domain(callback_query: AdminPanelCallback, state: FSMContext): +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( diff --git a/handlers/admin/panel/panel_handler.py b/handlers/admin/panel/panel_handler.py index 328eb047..9d863249 100644 --- a/handlers/admin/panel/panel_handler.py +++ b/handlers/admin/panel/panel_handler.py @@ -5,7 +5,7 @@ from aiogram.types import CallbackQuery, Message from bot import version from filters.admin import IsAdminFilter -from keyboard import AdminPanelCallback, build_panel_kb +from .keyboard import AdminPanelCallback, build_panel_kb router = Router() diff --git a/handlers/admin/restart/restart_handler.py b/handlers/admin/restart/restart_handler.py index b477f776..c60d969d 100644 --- a/handlers/admin/restart/restart_handler.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 handlers.admin.panel.keyboard 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/keyboard.py b/handlers/admin/sender/keyboard.py index 97e92710..2bfa4606 100644 --- a/handlers/admin/sender/keyboard.py +++ b/handlers/admin/sender/keyboard.py @@ -16,9 +16,7 @@ def build_sender_kb() -> InlineKeyboardMarkup: 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() - ), + InlineKeyboardButton(text="❌ Без подписки", callback_data=AdminSenderCallback(type="unsubscribed").pack()), ) builder.row( InlineKeyboardButton( diff --git a/handlers/admin/servers/keyboard.py b/handlers/admin/servers/keyboard.py index 9077ff4d..ab29be55 100644 --- a/handlers/admin/servers/keyboard.py +++ b/handlers/admin/servers/keyboard.py @@ -2,8 +2,6 @@ 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 @@ -11,6 +9,8 @@ class AdminServerCallback(CallbackData, prefix="admin_server"): 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()) diff --git a/handlers/admin/servers/servers_handler.py b/handlers/admin/servers/servers_handler.py index c5199bd6..e6bb8f3d 100644 --- a/handlers/admin/servers/servers_handler.py +++ b/handlers/admin/servers/servers_handler.py @@ -5,7 +5,7 @@ from aiogram.types import CallbackQuery from database import delete_server, get_servers from filters.admin import IsAdminFilter -from keyboard import ( +from .keyboard import ( AdminServerCallback, build_delete_server_kb, build_manage_server_kb, diff --git a/handlers/admin/stats/stats_handler.py b/handlers/admin/stats/stats_handler.py index 9707beb7..5afbbc14 100644 --- a/handlers/admin/stats/stats_handler.py +++ b/handlers/admin/stats/stats_handler.py @@ -7,7 +7,7 @@ from aiogram.exceptions import TelegramBadRequest from aiogram.types import CallbackQuery from filters.admin import IsAdminFilter -from keyboard 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 diff --git a/handlers/admin/users/users_handler.py b/handlers/admin/users/users_handler.py index c2dad526..7d169091 100644 --- a/handlers/admin/users/users_handler.py +++ b/handlers/admin/users/users_handler.py @@ -27,7 +27,7 @@ from handlers.utils import sanitize_key_name from logger import logger from utils.csv_export import export_referrals_csv from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb, build_admin_back_btn -from ..users.keyboard import ( +from .keyboard import ( AdminUserEditorCallback, AdminUserKeyEditorCallback, build_editor_kb,