From 73ba081779244e093d2517a8b4becc446f53ae6e Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Tue, 1 Apr 2025 22:10:40 +0300 Subject: [PATCH 01/10] =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=20=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D0=B2=D0=B0=D1=86=D0=B8=D0=B8=20=D0=BA=D1=83=D0=BF?= =?UTF-8?q?=D0=BE=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Корректировка состояния активации купона --- handlers/coupons.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/handlers/coupons.py b/handlers/coupons.py index 8632bd97..119e05e8 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -60,7 +60,9 @@ async def process_coupon_code(message: Message, state: FSMContext, session: Any) builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) await message.answer(activation_result, reply_markup=builder.as_markup()) - await state.clear() + + if activation_result.startswith(COUPON_ACTIVATED_SUCCESS_MSG.split('.')[0]): + await state.clear() async def activate_coupon(user_id: int, coupon_code: str, session: Any): From 4cfbcd859064ea81b355b9e8d77f160b876e4f6b Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Tue, 1 Apr 2025 23:34:12 +0300 Subject: [PATCH 02/10] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - добавление кнопки смены названия сервера --- handlers/admin/servers/keyboard.py | 1 + 1 file changed, 1 insertion(+) diff --git a/handlers/admin/servers/keyboard.py b/handlers/admin/servers/keyboard.py index d0a7cbea..4dc0abd1 100644 --- a/handlers/admin/servers/keyboard.py +++ b/handlers/admin/servers/keyboard.py @@ -15,6 +15,7 @@ def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboar builder = InlineKeyboardBuilder() builder.button(text="🗑️ Удалить", callback_data=AdminServerCallback(action="delete", data=server_name).pack()) + builder.button(text="✏️ Сменить название", callback_data=AdminServerCallback(action="rename", data=server_name).pack()) builder.button(text=BACK, callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack()) builder.adjust(1) return builder.as_markup() From a92b6d1e864cd0747f2f05676426e5da7c5e4359 Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Tue, 1 Apr 2025 23:35:19 +0300 Subject: [PATCH 03/10] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлена кнопка смены названия кластера --- handlers/admin/clusters/keyboard.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/handlers/admin/clusters/keyboard.py b/handlers/admin/clusters/keyboard.py index a2397d52..e267fa8b 100644 --- a/handlers/admin/clusters/keyboard.py +++ b/handlers/admin/clusters/keyboard.py @@ -93,6 +93,12 @@ def build_cluster_management_kb(cluster_name: str) -> InlineKeyboardMarkup: callback_data=AdminClusterCallback(action="add_time", data=cluster_name).pack(), ) ) + builder.row( + InlineKeyboardButton( + text="✏️ Сменить название", + callback_data=AdminClusterCallback(action="rename", data=cluster_name).pack(), + ) + ) builder.row( InlineKeyboardButton( text="🔙 Назад", From 2634facd51e3f529756eb605bda042d17cd7511f Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Tue, 1 Apr 2025 23:40:35 +0300 Subject: [PATCH 04/10] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B8?= =?UTF-8?q?=20=D1=81=D0=BC=D0=B5=D0=BD=D1=8B=20=D0=BD=D0=B0=D0=B7=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлена логика смены названия кластера/сервера - Сделана проверка на наличие ключей с названием кластера/сервера и смены на новое название --- handlers/admin/clusters/clusters_handler.py | 194 ++++++++++++++++++++ 1 file changed, 194 insertions(+) diff --git a/handlers/admin/clusters/clusters_handler.py b/handlers/admin/clusters/clusters_handler.py index 1c153731..9bad11ef 100644 --- a/handlers/admin/clusters/clusters_handler.py +++ b/handlers/admin/clusters/clusters_handler.py @@ -39,6 +39,8 @@ class AdminClusterStates(StatesGroup): waiting_for_server_name = State() waiting_for_subscription_url = State() waiting_for_days_input = State() + waiting_for_new_cluster_name = State() + waiting_for_new_server_name = State() @router.callback_query( @@ -538,3 +540,195 @@ async def handle_days_input(message: Message, state: FSMContext, session: Any): await message.answer("❌ Произошла ошибка при продлении времени.") finally: await state.clear() + +@router.callback_query(AdminClusterCallback.filter(F.action == "rename"), IsAdminFilter()) +async def handle_rename_cluster(callback_query: CallbackQuery, callback_data: AdminClusterCallback, state: FSMContext): + cluster_name = callback_data.data + await state.update_data(old_cluster_name=cluster_name) + + text = ( + f"✏️ Введите новое имя для кластера '{cluster_name}':\n\n" + "▸ Имя должно быть уникальным.\n" + "▸ Имя не должно превышать 12 символов.\n\n" + "📌 Пример: new_cluster" + ) + + await callback_query.message.edit_text( + text=text, + reply_markup=build_admin_back_kb("clusters"), + ) + await state.set_state(AdminClusterStates.waiting_for_new_cluster_name) + + +@router.message(AdminClusterStates.waiting_for_new_cluster_name, IsAdminFilter()) +async def handle_new_cluster_name_input(message: Message, state: FSMContext, session: Any): + if not message.text: + await message.answer( + text="❌ Имя кластера не может быть пустым! Попробуйте снова.", + reply_markup=build_admin_back_kb("clusters"), + ) + return + + new_cluster_name = message.text.strip() + if len(new_cluster_name) > 12: + await message.answer( + text="❌ Имя кластера не должно превышать 12 символов! Попробуйте снова.", + reply_markup=build_admin_back_kb("clusters"), + ) + return + + user_data = await state.get_data() + old_cluster_name = user_data.get("old_cluster_name") + + conn = await asyncpg.connect(DATABASE_URL) + try: + existing_cluster = await conn.fetchval( + "SELECT cluster_name FROM servers WHERE cluster_name = $1 LIMIT 1", + new_cluster_name + ) + if existing_cluster: + await message.answer( + text=f"❌ Кластер с именем '{new_cluster_name}' уже существует. Введите другое имя.", + reply_markup=build_admin_back_kb("clusters"), + ) + return + + keys_count = await conn.fetchval( + "SELECT COUNT(*) FROM keys WHERE server_id = $1", + old_cluster_name + ) + + async with conn.transaction(): + await conn.execute( + "UPDATE servers SET cluster_name = $1 WHERE cluster_name = $2", + new_cluster_name, + old_cluster_name + ) + + if keys_count > 0: + await conn.execute( + "UPDATE keys SET server_id = $1 WHERE server_id = $2", + new_cluster_name, + old_cluster_name + ) + + await message.answer( + text=f"✅ Название кластера успешно изменено с '{old_cluster_name}' на '{new_cluster_name}'!", + reply_markup=build_admin_back_kb("clusters"), + ) + except Exception as e: + logger.error(f"Ошибка при смене имени кластера {old_cluster_name} на {new_cluster_name}: {e}") + await message.answer( + text=f"❌ Произошла ошибка при смене имени кластера: {e}", + reply_markup=build_admin_back_kb("clusters"), + ) + finally: + await conn.close() + await state.clear() + + +@router.callback_query(AdminServerCallback.filter(F.action == "rename"), IsAdminFilter()) +async def handle_rename_server(callback_query: CallbackQuery, callback_data: AdminServerCallback, state: FSMContext): + old_server_name = callback_data.data + + servers = await get_servers() + cluster_name = None + for c_name, server_list in servers.items(): + for server in server_list: + if server["server_name"] == old_server_name: + cluster_name = c_name + break + if cluster_name: + break + + if not cluster_name: + await callback_query.message.edit_text( + text=f"❌ Не удалось найти кластер для сервера '{old_server_name}'.", + reply_markup=build_admin_back_kb("clusters"), + ) + return + + await state.update_data(old_server_name=old_server_name, cluster_name=cluster_name) + + text = ( + f"✏️ Введите новое имя для сервера '{old_server_name}' в кластере '{cluster_name}':\n\n" + "▸ Имя должно быть уникальным в пределах кластера.\n" + "▸ Имя не должно превышать 12 символов.\n\n" + "📌 Пример: new_server" + ) + + await callback_query.message.edit_text( + text=text, + reply_markup=build_admin_back_kb("clusters"), + ) + await state.set_state(AdminClusterStates.waiting_for_new_server_name) + + +@router.message(AdminClusterStates.waiting_for_new_server_name, IsAdminFilter()) +async def handle_new_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 + + new_server_name = message.text.strip() + if len(new_server_name) > 12: + await message.answer( + text="❌ Имя сервера не должно превышать 12 символов! Попробуйте снова.", + reply_markup=build_admin_back_kb("clusters"), + ) + return + + user_data = await state.get_data() + old_server_name = user_data.get("old_server_name") + cluster_name = user_data.get("cluster_name") + + conn = await asyncpg.connect(DATABASE_URL) + try: + existing_server = await conn.fetchval( + "SELECT server_name FROM servers WHERE cluster_name = $1 AND server_name = $2 LIMIT 1", + cluster_name, + new_server_name + ) + if existing_server: + await message.answer( + text=f"❌ Сервер с именем '{new_server_name}' уже существует в кластере '{cluster_name}'. Введите другое имя.", + reply_markup=build_admin_back_kb("clusters"), + ) + return + + keys_count = await conn.fetchval( + "SELECT COUNT(*) FROM keys WHERE server_id = $1", + old_server_name + ) + + async with conn.transaction(): + await conn.execute( + "UPDATE servers SET server_name = $1 WHERE cluster_name = $2 AND server_name = $3", + new_server_name, + cluster_name, + old_server_name + ) + + if keys_count > 0: + await conn.execute( + "UPDATE keys SET server_id = $1 WHERE server_id = $2", + new_server_name, + old_server_name + ) + + await message.answer( + text=f"✅ Название сервера успешно изменено с '{old_server_name}' на '{new_server_name}' в кластере '{cluster_name}'!", + reply_markup=build_admin_back_kb("clusters"), + ) + except Exception as e: + logger.error(f"Ошибка при смене имени сервера {old_server_name} на {new_server_name}: {e}") + await message.answer( + text=f"❌ Произошла ошибка при смене имени сервера: {e}", + reply_markup=build_admin_back_kb("clusters"), + ) + finally: + await conn.close() + await state.clear() From 0d06f61d4084922a4bf793d03d6e25f8398e66d8 Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Tue, 1 Apr 2025 23:55:27 +0300 Subject: [PATCH 05/10] =?UTF-8?q?=D0=94=D0=BE=D0=BF=20=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Стата за прошлый месяц --- handlers/admin/stats/stats_handler.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/handlers/admin/stats/stats_handler.py b/handlers/admin/stats/stats_handler.py index b2efe1df..8b7355ec 100644 --- a/handlers/admin/stats/stats_handler.py +++ b/handlers/admin/stats/stats_handler.py @@ -41,6 +41,16 @@ async def handle_stats(callback_query: CallbackQuery, session: Any): "SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('month', CURRENT_DATE)" ) ) + total_payments_last_month = int( + await session.fetchval( + """ + SELECT COALESCE(SUM(amount), 0) + FROM payments + WHERE created_at >= date_trunc('month', CURRENT_DATE - interval '1 month') + AND created_at < date_trunc('month', CURRENT_DATE) + """ + ) + ) total_payments_all_time = int(await session.fetchval("SELECT COALESCE(SUM(amount), 0) FROM payments")) all_keys = await session.fetch("SELECT created_at, expiry_time FROM keys") @@ -120,6 +130,7 @@ async def handle_stats(callback_query: CallbackQuery, session: Any): f"├ 📅 За день: {total_payments_today} ₽\n" f"├ 📆 За неделю: {total_payments_week} ₽\n" f"├ 📆 За месяц: {total_payments_month} ₽\n" + f"├ 📆 За прошлый месяц: {total_payments_last_month} ₽\n" f"└ 🏦 Всего: {total_payments_all_time} ₽\n\n" f"🔥 Горящие лиды: {hot_leads_count} (платили, но не продлили)\n\n" f"⏱️ Последнее обновление: {update_time}" From 812264722a84e43874a2a99e03c318fac5d3a7fd Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Wed, 2 Apr 2025 00:13:13 +0300 Subject: [PATCH 06/10] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20QR-=D0=BA=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен QR-код для реф ссылки. --- handlers/profile.py | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/handlers/profile.py b/handlers/profile.py index 82fe9ea3..8fe644b0 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -1,9 +1,11 @@ import html +from io import BytesIO import os -from typing import Any, Optional - import asyncpg +import qrcode + +from typing import Any, Optional from aiogram import F, Router from aiogram.enums import ParseMode @@ -48,6 +50,7 @@ from handlers.buttons import ( MAIN_MENU, MY_SUBS, PAYMENT, + QR, TOP_FIVE, ) from handlers.texts import BALANCE_HISTORY_HEADER, BALANCE_MANAGEMENT_TEXT, INVITE_TEXT_NON_INLINE, TOP_REFERRALS_TEXT @@ -250,6 +253,7 @@ async def invite_handler(callback_query_or_message: Message | CallbackQuery): else: invite_text = INVITE_TEXT_NON_INLINE.format(referral_link=referral_link) builder.button(text=INVITE, switch_inline_query=invite_text) + builder.button(text=QR, callback_data=f"show_referral_qr|{chat_id}") if TOP_REFERRAL_BUTTON: builder.button(text=TOP_FIVE, callback_data="top_referrals") builder.button(text=MAIN_MENU, callback_data="profile") @@ -290,6 +294,43 @@ async def inline_referral_handler(inline_query: InlineQuery): await inline_query.answer(results=results, cache_time=86400, is_personal=True) +@router.callback_query(F.data.startswith("show_referral_qr|")) +async def show_referral_qr(callback_query: CallbackQuery): + try: + chat_id = callback_query.data.split("|")[1] + referral_link = get_referral_link(chat_id) + + qr = qrcode.QRCode(version=1, box_size=10, border=4) + qr.add_data(referral_link) + qr.make(fit=True) + + img = qr.make_image(fill_color="black", back_color="white") + buffer = BytesIO() + img.save(buffer, format="PNG") + buffer.seek(0) + + qr_path = f"/tmp/qrcode_referral_{chat_id}.png" + with open(qr_path, "wb") as f: + f.write(buffer.read()) + + builder = InlineKeyboardBuilder() + builder.row(InlineKeyboardButton(text=BACK, callback_data="invite")) + builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile")) + + await edit_or_send_message( + target_message=callback_query.message, + text="📷 Ваш QR-код для реферальной ссылки.", + reply_markup=builder.as_markup(), + media_path=qr_path, + ) + + os.remove(qr_path) + + except Exception as e: + logger.error(f"Ошибка при генерации QR-кода для реферальной ссылки: {e}", exc_info=True) + await callback_query.message.answer("❌ Произошла ошибка при создании QR-кода.") + + @router.callback_query(F.data == "top_referrals") async def top_referrals_handler(callback_query: CallbackQuery): conn = await asyncpg.connect(DATABASE_URL) From bc81c64cbe2c5b47f12a06d7f512c42879cbd086 Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Wed, 2 Apr 2025 01:41:11 +0300 Subject: [PATCH 07/10] =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=BA=D0=B0=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Удалил ненужное --- handlers/admin/servers/keyboard.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/handlers/admin/servers/keyboard.py b/handlers/admin/servers/keyboard.py index 4dc0abd1..e74036ce 100644 --- a/handlers/admin/servers/keyboard.py +++ b/handlers/admin/servers/keyboard.py @@ -19,11 +19,3 @@ def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboar builder.button(text=BACK, 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=BACK, callback_data=AdminServerCallback(action="manage", data=server_name).pack()) - builder.adjust(1) - return builder.as_markup() From 476a8cb84212340a1d7aeeb8d0b00aff88315ecf Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Wed, 2 Apr 2025 01:42:12 +0300 Subject: [PATCH 08/10] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=BE=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Логика переноса ключей при удалении сервера/кластера --- handlers/admin/servers/servers_handler.py | 152 +++++++++++++++++++--- 1 file changed, 135 insertions(+), 17 deletions(-) diff --git a/handlers/admin/servers/servers_handler.py b/handlers/admin/servers/servers_handler.py index 849b5c92..db23c632 100644 --- a/handlers/admin/servers/servers_handler.py +++ b/handlers/admin/servers/servers_handler.py @@ -1,7 +1,10 @@ from typing import Any from aiogram import F, Router, types -from aiogram.types import CallbackQuery +from aiogram.fsm.context import FSMContext +from aiogram.types import CallbackQuery, InlineKeyboardButton +from aiogram.utils.keyboard import InlineKeyboardBuilder +from handlers.buttons import BACK from database import delete_server, get_servers from filters.admin import IsAdminFilter @@ -9,7 +12,6 @@ from filters.admin import IsAdminFilter from ..panel.keyboard import build_admin_back_kb from .keyboard import ( AdminServerCallback, - build_delete_server_kb, build_manage_server_kb, ) @@ -47,23 +49,139 @@ async def handle_server_manage(callback_query: CallbackQuery, callback_data: Adm @router.callback_query(AdminServerCallback.filter(F.action == "delete"), IsAdminFilter()) -async def handle_server_delete(callback_query: CallbackQuery, callback_data: AdminServerCallback): - server_name = callback_data.data - - await callback_query.message.edit_text( - text=f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?", - reply_markup=build_delete_server_kb(server_name), - ) - - -@router.callback_query(AdminServerCallback.filter(F.action == "delete_confirm"), IsAdminFilter()) -async def handle_server_delete_confirm( - callback_query: types.CallbackQuery, callback_data: AdminServerCallback, session: Any +async def process_callback_delete_server( + callback_query: CallbackQuery, callback_data: AdminServerCallback, state: FSMContext, session: Any ): + from ..clusters.clusters_handler import AdminClusterStates + server_name = callback_data.data - await delete_server(server_name, session) + servers = await get_servers(session) + cluster_name = None + for c_name, server_list in servers.items(): + for server in server_list: + if server["server_name"] == server_name: + cluster_name = c_name + break + if cluster_name: + break - await callback_query.message.edit_text( - text=f"🗑️ Сервер {server_name} успешно удален.", reply_markup=build_admin_back_kb("clusters") + if not cluster_name: + await callback_query.message.edit_text( + text=f"❌ Не удалось найти кластер для сервера '{server_name}'.", + reply_markup=build_admin_back_kb("clusters"), + ) + return + + keys_count = await session.fetchval( + "SELECT COUNT(*) FROM keys WHERE server_id = $1", + server_name ) + + if keys_count > 0: + await state.update_data(server_name=server_name, cluster_name=cluster_name) + + all_servers = await session.fetch( + """ + SELECT server_name, (SELECT COUNT(*) FROM keys WHERE server_id = servers.server_name) as key_count + FROM servers + WHERE server_name != $1 + """, + server_name + ) + + if all_servers: + builder = InlineKeyboardBuilder() + for server in all_servers: + builder.row( + InlineKeyboardButton( + text=f"{server['server_name']} ({server['key_count']})", + callback_data=f"transfer_to_server|{server['server_name']}|{server_name}" + ) + ) + builder.row( + InlineKeyboardButton( + text=BACK, + callback_data=AdminServerCallback(action="manage", data=server_name).pack() + ) + ) + + await callback_query.message.edit_text( + text=f"⚠️ На сервере '{server_name}' есть {keys_count} ключей. Выберите сервер для переноса ключей:", + reply_markup=builder.as_markup(), + ) + await state.set_state(AdminClusterStates.waiting_for_server_transfer) + return + + remaining_servers = await session.fetchval( + "SELECT COUNT(*) FROM servers WHERE cluster_name = $1 AND server_name != $2", + cluster_name, + server_name + ) + + if remaining_servers == 0: + other_clusters = await session.fetch( + "SELECT DISTINCT cluster_name FROM servers WHERE cluster_name != $1", + cluster_name + ) + + if other_clusters: + cluster_keys_count = await session.fetchval( + "SELECT COUNT(*) FROM keys WHERE server_id = $1", + cluster_name + ) + + if cluster_keys_count > 0: + await state.update_data(server_name=server_name, cluster_name=cluster_name) + + all_clusters = await session.fetch( + """ + SELECT cluster_name, (SELECT COUNT(*) FROM keys WHERE server_id = servers.cluster_name) as key_count + FROM servers + WHERE cluster_name != $1 + GROUP BY cluster_name + """, + cluster_name + ) + + builder = InlineKeyboardBuilder() + for cluster in all_clusters: + builder.row( + InlineKeyboardButton( + text=f"{cluster['cluster_name']} ({cluster['key_count']})", + callback_data=f"transfer_to_cluster|{cluster['cluster_name']}|{cluster_name}|{server_name}" + ) + ) + builder.row( + InlineKeyboardButton( + text=BACK, + callback_data=AdminServerCallback(action="manage", data=server_name).pack() + ) + ) + + await callback_query.message.edit_text( + text=f"⚠️ Это последний сервер в кластере '{cluster_name}'. На кластере есть {cluster_keys_count} ключей. Выберите кластер для переноса ключей:", + reply_markup=builder.as_markup(), + ) + await state.set_state(AdminClusterStates.waiting_for_cluster_transfer) + return + + await session.execute( + "DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", + cluster_name, + server_name + ) + await callback_query.message.edit_text( + text=f"✅ Сервер '{server_name}' удален. Кластер '{cluster_name}' также удален, так как в нем не осталось серверов.", + reply_markup=build_admin_back_kb("clusters"), + ) + else: + await session.execute( + "DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", + cluster_name, + server_name + ) + await callback_query.message.edit_text( + text=f"✅ Сервер '{server_name}' удален.", + reply_markup=build_admin_back_kb("clusters"), + ) From dc6f73c539c30e31a825cb6c605fda35b6aab827 Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Wed, 2 Apr 2025 01:43:04 +0300 Subject: [PATCH 09/10] =?UTF-8?q?=D0=9B=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=BE=D1=81=D0=B0=20=D0=BA=D0=BB?= =?UTF-8?q?=D1=8E=D1=87=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Логика переноса ключей при удалении сервера/кластера --- handlers/admin/clusters/clusters_handler.py | 87 +++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/handlers/admin/clusters/clusters_handler.py b/handlers/admin/clusters/clusters_handler.py index 9bad11ef..b0f7aa8c 100644 --- a/handlers/admin/clusters/clusters_handler.py +++ b/handlers/admin/clusters/clusters_handler.py @@ -41,6 +41,8 @@ class AdminClusterStates(StatesGroup): waiting_for_days_input = State() waiting_for_new_cluster_name = State() waiting_for_new_server_name = State() + waiting_for_server_transfer = State() + waiting_for_cluster_transfer = State() @router.callback_query( @@ -541,6 +543,7 @@ async def handle_days_input(message: Message, state: FSMContext, session: Any): finally: await state.clear() + @router.callback_query(AdminClusterCallback.filter(F.action == "rename"), IsAdminFilter()) async def handle_rename_cluster(callback_query: CallbackQuery, callback_data: AdminClusterCallback, state: FSMContext): cluster_name = callback_data.data @@ -732,3 +735,87 @@ async def handle_new_server_name_input(message: Message, state: FSMContext, sess finally: await conn.close() await state.clear() + + +@router.callback_query(F.data.startswith("transfer_to_server|")) +async def handle_server_transfer(callback_query: CallbackQuery, state: FSMContext): + data = callback_query.data.split("|") + new_server_name = data[1] + old_server_name = data[2] + + user_data = await state.get_data() + cluster_name = user_data.get("cluster_name") + + conn = await asyncpg.connect(DATABASE_URL) + try: + async with conn.transaction(): + await conn.execute( + "UPDATE keys SET server_id = $1 WHERE server_id = $2", + new_server_name, + old_server_name + ) + + await conn.execute( + "DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", + cluster_name, + old_server_name + ) + + await callback_query.message.edit_text( + text=f"✅ Ключи успешно перенесены на сервер '{new_server_name}', сервер '{old_server_name}' удален!", + reply_markup=build_admin_back_kb("clusters"), + ) + except Exception as e: + logger.error(f"Ошибка при переносе ключей на сервер {new_server_name}: {e}") + await callback_query.message.edit_text( + text=f"❌ Произошла ошибка при переносе ключей: {e}", + reply_markup=build_admin_back_kb("clusters"), + ) + finally: + await conn.close() + await state.clear() + + +@router.callback_query(F.data.startswith("transfer_to_cluster|")) +async def handle_cluster_transfer(callback_query: CallbackQuery, state: FSMContext): + data = callback_query.data.split("|") + new_cluster_name = data[1] + old_cluster_name = data[2] + old_server_name = data[3] + + user_data = await state.get_data() + cluster_name = user_data.get("cluster_name") + + conn = await asyncpg.connect(DATABASE_URL) + try: + async with conn.transaction(): + await conn.execute( + "UPDATE keys SET server_id = $1 WHERE server_id = $2", + new_cluster_name, + old_server_name + ) + await conn.execute( + "UPDATE keys SET server_id = $1 WHERE server_id = $2", + new_cluster_name, + old_cluster_name + ) + + await conn.execute( + "DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", + cluster_name, + old_server_name + ) + + await callback_query.message.edit_text( + text=f"✅ Ключи успешно перенесены в кластер '{new_cluster_name}', сервер '{old_server_name}' и кластер '{old_cluster_name}' удалены!", + reply_markup=build_admin_back_kb("clusters"), + ) + except Exception as e: + logger.error(f"Ошибка при переносе ключей в кластер {new_cluster_name}: {e}") + await callback_query.message.edit_text( + text=f"❌ Произошла ошибка при переносе ключей: {e}", + reply_markup=build_admin_back_kb("clusters"), + ) + finally: + await conn.close() + await state.clear() From c3d71bdc285f48054dd2197810246962e108fbfb Mon Sep 17 00:00:00 2001 From: Capybara-z Date: Wed, 2 Apr 2025 02:08:09 +0300 Subject: [PATCH 10/10] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлены напоминание синхронизации. --- handlers/admin/clusters/clusters_handler.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/handlers/admin/clusters/clusters_handler.py b/handlers/admin/clusters/clusters_handler.py index b0f7aa8c..a169d8b2 100644 --- a/handlers/admin/clusters/clusters_handler.py +++ b/handlers/admin/clusters/clusters_handler.py @@ -12,7 +12,7 @@ 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, TOTAL_GB +from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, TOTAL_GB, USE_COUNTRY_SELECTION from database import check_unique_server_name, get_servers, update_key_expiry from filters.admin import IsAdminFilter from handlers.keys.key_utils import create_client_on_server, create_key_on_cluster, renew_key_in_cluster @@ -616,7 +616,7 @@ async def handle_new_cluster_name_input(message: Message, state: FSMContext, ses ) await message.answer( - text=f"✅ Название кластера успешно изменено с '{old_cluster_name}' на '{new_cluster_name}'!", + text=f"✅ Название кластера успешно изменено с '{old_cluster_name}' на '{new_cluster_name}'!\n\n⚠️ Не забудьте сделать \"Синхронизацию\".", reply_markup=build_admin_back_kb("clusters"), ) except Exception as e: @@ -722,8 +722,13 @@ async def handle_new_server_name_input(message: Message, state: FSMContext, sess old_server_name ) + # Формируем текст сообщения с учетом USE_COUNTRY_SELECTION + base_text = f"✅ Название сервера успешно изменено с '{old_server_name}' на '{new_server_name}' в кластере '{cluster_name}'!" + sync_reminder = "\n\n⚠️ Не забудьте сделать \"Синхронизацию\"." + final_text = base_text + (sync_reminder if USE_COUNTRY_SELECTION else "") + await message.answer( - text=f"✅ Название сервера успешно изменено с '{old_server_name}' на '{new_server_name}' в кластере '{cluster_name}'!", + text=final_text, reply_markup=build_admin_back_kb("clusters"), ) except Exception as e: @@ -761,8 +766,12 @@ async def handle_server_transfer(callback_query: CallbackQuery, state: FSMContex old_server_name ) + base_text = f"✅ Ключи успешно перенесены на сервер '{new_server_name}', сервер '{old_server_name}' удален!" + sync_reminder = "\n\n⚠️ Не забудьте сделать \"Синхронизацию\"." + final_text = base_text + (sync_reminder if USE_COUNTRY_SELECTION else "") + await callback_query.message.edit_text( - text=f"✅ Ключи успешно перенесены на сервер '{new_server_name}', сервер '{old_server_name}' удален!", + text=final_text, reply_markup=build_admin_back_kb("clusters"), ) except Exception as e: @@ -807,7 +816,7 @@ async def handle_cluster_transfer(callback_query: CallbackQuery, state: FSMConte ) await callback_query.message.edit_text( - text=f"✅ Ключи успешно перенесены в кластер '{new_cluster_name}', сервер '{old_server_name}' и кластер '{old_cluster_name}' удалены!", + text=f"✅ Ключи успешно перенесены в кластер '{new_cluster_name}', сервер '{old_server_name}' и кластер '{old_cluster_name}' удалены!\n\n⚠️ Не забудьте сделать \"Синхронизацию\".", reply_markup=build_admin_back_kb("clusters"), ) except Exception as e: