From 993c79e9db17408e3b1e981a990f5988e8eb7d6e Mon Sep 17 00:00:00 2001 From: hteppl Date: Fri, 31 Jan 2025 04:21:03 +0300 Subject: [PATCH 1/8] Fix type mismatch for build_admin_back_btn --- keyboards/admin/panel_kb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keyboards/admin/panel_kb.py b/keyboards/admin/panel_kb.py index 2c23381d..b4388d57 100644 --- a/keyboards/admin/panel_kb.py +++ b/keyboards/admin/panel_kb.py @@ -53,7 +53,7 @@ def build_admin_singleton_kb(text: str, action: str) -> InlineKeyboardMarkup: return builder.as_markup() -def build_admin_back_btn(action: str = "admin") -> InlineKeyboardMarkup: +def build_admin_back_btn(action: str = "admin") -> InlineKeyboardButton: return build_admin_btn("🔙 Назад", action) From ba692f5b73a77fbd0eb2b2629e33efd329629d9b Mon Sep 17 00:00:00 2001 From: hteppl Date: Fri, 31 Jan 2025 05:03:07 +0300 Subject: [PATCH 2/8] Get back cluster sender option --- handlers/admin/admin_sender.py | 28 ++++++++++++++++++++++++++-- keyboards/admin/sender_kb.py | 18 ++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/handlers/admin/admin_sender.py b/handlers/admin/admin_sender.py index 4e8a92cf..7987598c 100644 --- a/handlers/admin/admin_sender.py +++ b/handlers/admin/admin_sender.py @@ -8,7 +8,7 @@ from aiogram.types import CallbackQuery from filters.admin import IsAdminFilter from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb -from keyboards.admin.sender_kb import AdminSenderCallback, build_sender_kb +from keyboards.admin.sender_kb import AdminSenderCallback, build_sender_kb, build_clusters_kb from logger import logger router = Router() @@ -38,10 +38,22 @@ async def handle_sender_callback(callback_query: CallbackQuery, callback_data: A text="✍️ Введите текст сообщения для рассылки:", reply_markup=build_admin_back_kb("sender"), ) - await state.update_data(type=callback_data.type) + await state.update_data(type=callback_data.type, data=callback_data.data) await state.set_state(AdminSender.waiting_for_message) +@router.callback_query( + AdminSenderCallback.filter(F.type == "cluster-select"), + IsAdminFilter(), +) +async def handle_sender_callback(callback_query: CallbackQuery, session: Any): + clusters = await session.fetch("SELECT DISTINCT cluster_name FROM servers") + await callback_query.message.answer( + "✍️ Выберите кластер для рассылки сообщений:", + reply_markup=build_clusters_kb(clusters), + ) + + @router.message( AdminSender.waiting_for_message, IsAdminFilter(), @@ -74,6 +86,18 @@ async def handle_message_input(message: types.Message, state: FSMContext, sessio """, int(datetime.utcnow().timestamp() * 1000), ) + elif send_to == "cluster": + cluster_name = state_data.get("data") + tg_ids = await session.fetch( + """ + SELECT DISTINCT c.tg_id + FROM connections c + JOIN keys k ON c.tg_id = k.tg_id + JOIN servers s ON k.server_id = s.cluster_name + WHERE s.cluster_name = $1 + """, + cluster_name, + ) else: tg_ids = await session.fetch("SELECT DISTINCT tg_id FROM connections") diff --git a/keyboards/admin/sender_kb.py b/keyboards/admin/sender_kb.py index d0acfc59..bf29f29c 100644 --- a/keyboards/admin/sender_kb.py +++ b/keyboards/admin/sender_kb.py @@ -1,3 +1,5 @@ +from typing import Union + from aiogram.filters.callback_data import CallbackData from aiogram.types import InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder @@ -7,6 +9,7 @@ from keyboards.admin.panel_kb import build_admin_back_btn class AdminSenderCallback(CallbackData, prefix="admin_sender"): type: str + data: Union[str, None] = None def build_sender_kb() -> InlineKeyboardMarkup: @@ -14,6 +17,21 @@ def build_sender_kb() -> InlineKeyboardMarkup: builder.button(text="👥 Все пользователи", callback_data=AdminSenderCallback(type="all").pack()) builder.button(text="✅ Пользователи с подпиской", callback_data=AdminSenderCallback(type="subscribed").pack()) builder.button(text="❌ Пользователи без подписки", callback_data=AdminSenderCallback(type="unsubscribed").pack()) + builder.button(text="📢 Пользователи кластера", callback_data=AdminSenderCallback(type="cluster-select").pack()) + builder.row(build_admin_back_btn()) + builder.adjust(1) + return builder.as_markup() + + +def build_clusters_kb(clusters: list) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + for cluster in clusters: + name = cluster['cluster_name'] + builder.button( + text=f"🌐 {name}", + callback_data=AdminSenderCallback(type="cluster", data=name).pack() + ) + builder.row(build_admin_back_btn()) builder.adjust(1) return builder.as_markup() From 85f26425ed6fca7ddffb7b67f6eff023b408d8c3 Mon Sep 17 00:00:00 2001 From: hteppl Date: Fri, 31 Jan 2025 05:14:40 +0300 Subject: [PATCH 3/8] Get back cluster sync option --- handlers/admin/admin_servers.py | 63 ++++++++++++++++++++++++++++++--- keyboards/admin/servers_kb.py | 4 +++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index 9632b424..68b000fd 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -1,3 +1,4 @@ +import asyncio from typing import Any import asyncpg @@ -10,6 +11,7 @@ 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, 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 ( AdminServerEditorCallback, @@ -18,6 +20,7 @@ from keyboards.admin.servers_kb import ( build_manage_cluster_kb, build_manage_server_kb, ) +from logger import logger router = Router() @@ -229,7 +232,7 @@ async def handle_inbound_id_input(message: types.Message, state: FSMContext): @router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_manage"), IsAdminFilter()) async def handle_clusters_manage( - callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any ): cluster_name = callback_data.data @@ -244,7 +247,7 @@ async def handle_clusters_manage( @router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_availability"), IsAdminFilter()) async def handle_servers_availability( - callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any ): cluster_name = callback_data.data @@ -320,7 +323,7 @@ async def handle_servers_delete(callback_query: types.CallbackQuery, callback_da @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 + callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any ): server_name = callback_data.data @@ -333,7 +336,7 @@ async def handle_servers_delete_confirm( @router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_add"), IsAdminFilter()) async def handle_servers_add( - callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, state: FSMContext + callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, state: FSMContext ): cluster_name = callback_data.data @@ -355,7 +358,7 @@ async def handle_servers_add( @router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_backup"), IsAdminFilter()) async def handle_clusters_backup( - callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any ): cluster_name = callback_data.data @@ -379,3 +382,53 @@ async def handle_clusters_backup( text=text, reply_markup=build_admin_back_kb("servers"), ) + + +@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_sync"), IsAdminFilter()) +async def handle_clusters_backup( + 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 + + servers = await get_servers(session) + cluster_servers = servers.get(cluster_name, []) + + for key in keys_to_sync: + for _server in cluster_servers: + 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") + ) diff --git a/keyboards/admin/servers_kb.py b/keyboards/admin/servers_kb.py index e857408b..c3498724 100644 --- a/keyboards/admin/servers_kb.py +++ b/keyboards/admin/servers_kb.py @@ -46,6 +46,10 @@ def build_manage_cluster_kb(cluster_servers, cluster_name) -> InlineKeyboardMark text="💾 Создать бэкап кластера", callback_data=AdminServerEditorCallback(action="clusters_backup", data=cluster_name).pack(), ) + builder.button( + text="🔄 Синхронизировать", + callback_data=AdminServerEditorCallback(action="clusters_sync", data=cluster_name).pack(), + ) builder.row(build_admin_back_btn("servers")) builder.adjust(1) return builder.as_markup() From 70bf8b98e97e07c028b2ab0bae1ecc91a4bcee6a Mon Sep 17 00:00:00 2001 From: hteppl Date: Fri, 31 Jan 2025 05:19:52 +0300 Subject: [PATCH 4/8] Oops legacy codestyle --- keyboards/admin/sender_kb.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/keyboards/admin/sender_kb.py b/keyboards/admin/sender_kb.py index bf29f29c..9d1055f0 100644 --- a/keyboards/admin/sender_kb.py +++ b/keyboards/admin/sender_kb.py @@ -1,5 +1,3 @@ -from typing import Union - from aiogram.filters.callback_data import CallbackData from aiogram.types import InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder @@ -9,7 +7,7 @@ from keyboards.admin.panel_kb import build_admin_back_btn class AdminSenderCallback(CallbackData, prefix="admin_sender"): type: str - data: Union[str, None] = None + data: str | None = None def build_sender_kb() -> InlineKeyboardMarkup: From f042c5fd439b322fbb674b499c574754f25165db Mon Sep 17 00:00:00 2001 From: hteppl Date: Fri, 31 Jan 2025 06:10:37 +0300 Subject: [PATCH 5/8] Fix cluster selection --- handlers/admin/admin_sender.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/handlers/admin/admin_sender.py b/handlers/admin/admin_sender.py index 7987598c..0c298a85 100644 --- a/handlers/admin/admin_sender.py +++ b/handlers/admin/admin_sender.py @@ -30,7 +30,7 @@ async def handle_sender(callback_query: CallbackQuery): @router.callback_query( - AdminSenderCallback.filter(), + AdminSenderCallback.filter(F.type != "cluster-select"), IsAdminFilter(), ) async def handle_sender_callback(callback_query: CallbackQuery, callback_data: AdminSenderCallback, state: FSMContext): @@ -38,7 +38,7 @@ async def handle_sender_callback(callback_query: CallbackQuery, callback_data: A text="✍️ Введите текст сообщения для рассылки:", reply_markup=build_admin_back_kb("sender"), ) - await state.update_data(type=callback_data.type, data=callback_data.data) + await state.update_data(type=callback_data.type, cluster_name=callback_data.data) await state.set_state(AdminSender.waiting_for_message) @@ -87,7 +87,7 @@ async def handle_message_input(message: types.Message, state: FSMContext, sessio int(datetime.utcnow().timestamp() * 1000), ) elif send_to == "cluster": - cluster_name = state_data.get("data") + cluster_name = state_data.get("cluster_name") tg_ids = await session.fetch( """ SELECT DISTINCT c.tg_id From d83ade8d5129ac48da4881aebfcd4b5c8e05ca4f Mon Sep 17 00:00:00 2001 From: hteppl Date: Fri, 31 Jan 2025 06:45:24 +0300 Subject: [PATCH 6/8] Delete admin_user_editor.py --- handlers/admin/admin_user_editor.py | 592 ---------------------------- 1 file changed, 592 deletions(-) delete mode 100644 handlers/admin/admin_user_editor.py diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py deleted file mode 100644 index 4d4ff573..00000000 --- a/handlers/admin/admin_user_editor.py +++ /dev/null @@ -1,592 +0,0 @@ -import asyncio -from datetime import datetime -from typing import Any - -import pytz -from aiogram import Bot, F, Router, types -from aiogram.fsm.context import FSMContext -from aiogram.fsm.state import State, StatesGroup -from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup -from aiogram.utils.keyboard import InlineKeyboardBuilder - -from config import TOTAL_GB -from database import ( - delete_key, - delete_user_data, - get_client_id_by_email, - get_key_details, - get_keys, - get_servers, - update_key_expiry, - update_trial, -) -from filters.admin import IsAdminFilter -from handlers.keys.key_utils import ( - delete_key_from_cluster, - renew_key_in_cluster, -) -from handlers.utils import sanitize_key_name -from logger import logger - -router = Router() - - -class UserEditorState(StatesGroup): - waiting_for_tg_id = State() - waiting_for_username = State() - displaying_user_info = State() - waiting_for_new_balance = State() - waiting_for_key_name = State() - waiting_for_expiry_time = State() - waiting_for_message_text = State() - - -@router.callback_query(F.data == "search_by_tg_id", IsAdminFilter()) -async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext): - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - await callback_query.message.answer("🔍 Введите Telegram ID клиента:", reply_markup=builder.as_markup()) - await state.set_state(UserEditorState.waiting_for_tg_id) - - -@router.callback_query(F.data == "search_by_username", IsAdminFilter()) -async def prompt_username(callback_query: CallbackQuery, state: FSMContext): - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - await callback_query.message.answer("🔍 Введите Username клиента:", reply_markup=builder.as_markup()) - await state.set_state(UserEditorState.waiting_for_username) - - -@router.message(UserEditorState.waiting_for_username, IsAdminFilter()) -async def handle_username_input(message: types.Message, state: FSMContext, session: Any): - username = message.text.strip().lstrip("@").replace("https://t.me/", "") - user_record = await session.fetchrow("SELECT tg_id FROM users WHERE username = $1", username) - - if not user_record: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - await message.answer( - "🔍 Пользователь с указанным username не найден. 🚫", - reply_markup=builder.as_markup(), - ) - await state.clear() - return - - tg_id = user_record["tg_id"] - username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id) - balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) - key_records = await get_keys(tg_id, session) - referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) - - if balance is None: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - await message.answer( - "🚫 Пользователь с указанным tg_id не найден. 🔍", - reply_markup=builder.as_markup(), - ) - await state.clear() - return - - builder = InlineKeyboardBuilder() - - for (email,) in key_records: - builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")) - - builder.row( - InlineKeyboardButton( - text="📝 Изменить баланс", - callback_data=f"change_balance_{tg_id}", - ) - ) - - builder.row( - InlineKeyboardButton( - text="🔄 Восстановить пробник", - callback_data=f"restore_trial_{tg_id}", - ) - ) - builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}")) - builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}")) - builder.row(InlineKeyboardButton(text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}")) - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - - user_info = ( - f"📊 Информация о пользователе:\n\n" - f"🆔 ID пользователя: {tg_id}\n" - f"👤 Логин пользователя: @{username}\n" - f"💰 Баланс: {balance}\n" - f"👥 Количество рефералов: {referral_count}\n" - f"🔑 Ключи (для редактирования нажмите на ключ):" - ) - await message.answer(user_info, reply_markup=builder.as_markup()) - await state.set_state(UserEditorState.displaying_user_info) - - -@router.callback_query(F.data.startswith("send_message_")) -async def handle_send_message(callback_query: types.CallbackQuery, state: FSMContext): - tg_id = callback_query.data.split("_")[2] - await state.update_data(target_tg_id=tg_id) - await callback_query.message.answer("✉️ Введите текст сообщения, которое вы хотите отправить пользователю.") - await state.set_state(UserEditorState.waiting_for_message_text) - - -@router.message(UserEditorState.waiting_for_message_text, IsAdminFilter()) -async def process_send_message(message: types.Message, state: FSMContext, bot: Bot): - data = await state.get_data() - target_tg_id = data.get("target_tg_id") - - if not target_tg_id: - await message.answer("🚫 Ошибка: ID пользователя не найден.") - await state.clear() - return - - try: - await bot.send_message(chat_id=target_tg_id, text=message.text) - await message.answer("✅ Сообщение успешно отправлено.") - except Exception as e: - await message.answer(f"❌ Не удалось отправить сообщение: {e}") - - await state.clear() - - -@router.message(UserEditorState.waiting_for_tg_id, F.text.isdigit(), IsAdminFilter()) -async def handle_tg_id_input(message: types.Message, state: FSMContext, session: Any): - tg_id = int(message.text) - username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id) - balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) - key_records = await get_keys(tg_id, session) - referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) - - if balance is None: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - await message.answer( - "❌ Пользователь с указанным tg_id не найден. 🔍", - reply_markup=builder.as_markup(), - ) - await state.clear() - return - - builder = InlineKeyboardBuilder() - - for (email,) in key_records: - builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")) - - builder.row( - InlineKeyboardButton( - text="📝 Изменить баланс", - callback_data=f"change_balance_{tg_id}", - ) - ) - builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}")) - builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}")) - builder.row( - InlineKeyboardButton( - text="🔄 Восстановить пробник", - callback_data=f"restore_trial_{tg_id}", - ) - ) - builder.row(InlineKeyboardButton(text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}")) - - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - - user_info = ( - f"📊 Информация о пользователе:\n\n" - f"🆔 ID пользователя: {tg_id}\n" - f"👤 Логин пользователя: @{username}\n" - f"💰 Баланс: {balance}\n" - f"👥 Количество рефералов: {referral_count}\n" - f"🔑 Ключи (для редактирования нажмите на ключ):" - ) - await message.answer(user_info, reply_markup=builder.as_markup()) - await state.set_state(UserEditorState.displaying_user_info) - - -@router.callback_query(F.data.startswith("restore_trial_"), IsAdminFilter()) -async def handle_restore_trial(callback_query: types.CallbackQuery, session: Any): - tg_id = int(callback_query.data.split("_")[2]) - - await update_trial(tg_id, 0, session) - - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад в меню администратора", callback_data="admin")) - - await callback_query.message.answer("✅ Триал успешно восстановлен.", reply_markup=builder.as_markup()) - - -@router.callback_query(F.data.startswith("change_balance_"), IsAdminFilter()) -async def process_balance_change(callback_query: CallbackQuery, state: FSMContext): - tg_id = int(callback_query.data.split("_")[2]) - await state.update_data(tg_id=tg_id) - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - await callback_query.message.answer("💸 Введите новую сумму баланса:", reply_markup=builder.as_markup()) - await state.set_state(UserEditorState.waiting_for_new_balance) - - -@router.message(UserEditorState.waiting_for_new_balance, IsAdminFilter()) -async def handle_new_balance_input(message: types.Message, state: FSMContext, session: Any): - if not message.text.isdigit() or int(message.text) < 0: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - await message.answer( - "❌ Пожалуйста, введите корректную сумму для изменения баланса.", - reply_markup=builder.as_markup(), - ) - return - - new_balance = int(message.text) - user_data = await state.get_data() - tg_id = user_data.get("tg_id") - - await session.execute( - "UPDATE connections SET balance = $1 WHERE tg_id = $2", - new_balance, - tg_id, - ) - - response_message = f"✅ Баланс успешно изменен на {new_balance}." - - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="⬅️ Назад в меню администратора", - callback_data="admin", - ) - ) - await message.answer(response_message, reply_markup=builder.as_markup()) - await state.clear() - - -@router.callback_query(F.data.startswith("edit_key_"), IsAdminFilter()) -async def process_key_edit(callback_query: CallbackQuery, session: Any): - email = callback_query.data.split("_", 2)[2] - key_details = await get_key_details(email, session) - - if not key_details: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - await callback_query.message.answer( - "🔍 Информация о ключе не найдена. 🚫", - reply_markup=builder.as_markup(), - ) - return - - response_message = ( - f"🔑 Ключ: {key_details['key']}\n" - f"⏰ Дата истечения: {key_details['expiry_date']}\n" - f"💰 Баланс пользователя: {key_details['balance']}\n" - f"🌐 Кластер: {key_details['server_name']}" - ) - - builder = InlineKeyboardBuilder() - builder.row( - InlineKeyboardButton( - text="ℹ️ Получить информацию о юзере", - callback_data=f"user_info|{key_details['tg_id']}", - ) - ) - builder.row( - InlineKeyboardButton( - text="⏳ Изменить время истечения", - callback_data=f"change_expiry|{email}", - ) - ) - builder.row( - InlineKeyboardButton( - text="❌ Удалить ключ", - callback_data=f"delete_key_admin|{email}", - ) - ) - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - - await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) - - -@router.callback_query(F.data == "search_by_key_name", IsAdminFilter()) -async def prompt_key_name(callback_query: CallbackQuery, state: FSMContext): - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - await callback_query.message.answer("🔑 Введите имя ключа:", reply_markup=builder.as_markup()) - await state.set_state(UserEditorState.waiting_for_key_name) - - -@router.message(UserEditorState.waiting_for_key_name, IsAdminFilter()) -async def handle_key_name_input(message: types.Message, state: FSMContext, session: Any): - key_name = sanitize_key_name(message.text) - key_details = await get_key_details(key_name, session) - - if not key_details: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - await message.answer( - "🚫 Пользователь с указанным именем ключа не найден.", - reply_markup=builder.as_markup(), - ) - await state.clear() - return - - response_message = ( - f"🔑 Ключ: {key_details['key']}\n" - f"⏰ Дата истечения: {key_details['expiry_date']}\n" - f"💰 Баланс пользователя: {key_details['balance']}\n" - f"🌐 Сервер: {key_details['server_name']}" - ) - - key_buttons = InlineKeyboardBuilder() - key_buttons.row( - InlineKeyboardButton( - text="ℹ️ Получить информацию о юзере", - callback_data=f"user_info|{key_details['tg_id']}", - ) - ) - key_buttons.row( - InlineKeyboardButton( - text="⏳ Изменить время истечения", - callback_data=f"change_expiry|{key_name}", - ) - ) - key_buttons.row( - InlineKeyboardButton( - text="❌ Удалить ключ", - callback_data=f"delete_key_admin|{key_name}", - ) - ) - key_buttons.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - - await message.answer(response_message, reply_markup=key_buttons.as_markup()) - await state.clear() - - -@router.callback_query(F.data.startswith("change_expiry|"), IsAdminFilter()) -async def prompt_expiry_change(callback_query: CallbackQuery, state: FSMContext): - email = callback_query.data.split("|")[1] - await callback_query.message.answer( - f"⏳ Введите новое время истечения для ключа {email} в формате YYYY-MM-DD HH:MM:SS:" - ) - await state.update_data(email=email) - await state.set_state(UserEditorState.waiting_for_expiry_time) - - -@router.message(UserEditorState.waiting_for_expiry_time, IsAdminFilter()) -async def handle_expiry_time_input(message: types.Message, state: FSMContext, session: Any): - user_data = await state.get_data() - email = user_data.get("email") - - if not email: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - await message.answer("📧 Email не найден в состоянии. 🚫", reply_markup=builder.as_markup()) - await state.clear() - return - - try: - expiry_time_str = message.text - moscow_tz = pytz.timezone("Europe/Moscow") - expiry_time = int( - datetime.strptime(expiry_time_str, "%Y-%m-%d %H:%M:%S").replace(tzinfo=moscow_tz).timestamp() * 1000 - ) - - client_id = await get_client_id_by_email(email) - if client_id is None: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - await message.answer( - f"🚫 Клиент с email {email} не найден. 🔍", - reply_markup=builder.as_markup(), - ) - await state.clear() - return - - record = await get_key_details(email, session) - if not record: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - await message.answer( - "🚫 Клиент не найден в базе данных. 🔍", - reply_markup=builder.as_markup(), - ) - await state.clear() - return - - clusters = await get_servers(session) - - async def update_key_on_all_servers(): - tasks = [] - for cluster_name, cluster_servers in clusters.items(): - for _server in cluster_servers: - tasks.append( - asyncio.create_task( - renew_key_in_cluster( - cluster_name, - email, - client_id, - expiry_time, - total_gb=TOTAL_GB, - ) - ) - ) - await asyncio.gather(*tasks) - - await update_key_on_all_servers() - - await update_key_expiry(client_id, expiry_time, session) - - response_message = ( - f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах." - ) - - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="admin")) - await message.answer(response_message, reply_markup=builder.as_markup()) - except ValueError: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - await message.answer( - "❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.", - reply_markup=builder.as_markup(), - ) - except Exception as e: - logger.error(e) - await state.clear() - - -@router.callback_query(F.data.startswith("delete_key_admin|"), IsAdminFilter()) -async def process_callback_delete_key(callback_query: types.CallbackQuery, session: Any): - email = callback_query.data.split("|")[1] - key_details = await get_key_details(email, session) - - if key_details is None: - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - await callback_query.message.answer("🔍 Ключ не найден. 🚫", reply_markup=builder.as_markup()) - return - - builder = InlineKeyboardBuilder() - builder.row( - types.InlineKeyboardButton( - text="✅ Да, удалить", - callback_data=f"confirm_delete_admin|{key_details['client_id']}", - ) - ) - builder.row(types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="user_editor")) - await callback_query.message.answer( - "❓ Вы уверены, что хотите удалить ключ?", - reply_markup=builder.as_markup(), - ) - - -@router.callback_query(F.data.startswith("confirm_delete_admin|"), IsAdminFilter()) -async def process_callback_confirm_delete(callback_query: types.CallbackQuery, session: Any): - client_id = callback_query.data.split("|")[1] - record = await session.fetchrow("SELECT email FROM keys WHERE client_id = $1", client_id) - - if record: - email = record["email"] - response_message = "✅ Ключ успешно удален." - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys")) - - clusters = await get_servers(session) - - async def delete_key_from_servers(email, client_id): - tasks = [] - for cluster_name, cluster_servers in clusters.items(): - for _server in cluster_servers: - tasks.append(delete_key_from_cluster(cluster_name, email, client_id)) - await asyncio.gather(*tasks) - - await delete_key_from_servers(email, client_id) - await delete_key(client_id, session) - - await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) - else: - response_message = "🚫 Ключ не найден или уже удален." - builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys")) - await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) - - -@router.callback_query(F.data.startswith("user_info|"), IsAdminFilter()) -async def handle_user_info(callback_query: types.CallbackQuery, state: FSMContext, session: Any): - tg_id = int(callback_query.data.split("|")[1]) - username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id) - balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) - key_records = await get_keys(tg_id, session) - referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) - - builder = InlineKeyboardBuilder() - - for (email,) in key_records: - builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")) - - builder.row(InlineKeyboardButton(text="📝 Изменить баланс", callback_data=f"change_balance_{tg_id}")) - builder.row(InlineKeyboardButton(text="🔄 Восстановить пробник", callback_data=f"restore_trial_{tg_id}")) - builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}")) - builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}")) - builder.row(InlineKeyboardButton(text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}")) - builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor")) - - user_info = ( - f"📊 Информация о пользователе:\n\n" - f"🆔 ID пользователя: {tg_id}\n" - f"👤 Логин пользователя: @{username}\n" - f"💰 Баланс: {balance}\n" - f"👥 Количество рефералов: {referral_count}\n" - f"🔑 Ключи (для редактирования нажмите на ключ):" - ) - - await callback_query.message.answer(user_info, reply_markup=builder.as_markup()) - await state.set_state(UserEditorState.displaying_user_info) - - -@router.callback_query(F.data.startswith("confirm_delete_user_"), IsAdminFilter()) -async def confirm_delete_user(callback_query: types.CallbackQuery, state: FSMContext, session: Any): - tg_id = int(callback_query.data.split("_")[3]) - - confirmation_markup = InlineKeyboardMarkup( - row_width=2, - inline_keyboard=[ - [InlineKeyboardButton(text="✅ Подтвердить", callback_data=f"delete_user_{tg_id}")], - [InlineKeyboardButton(text="❌ Отменить", callback_data="user_editor")], - ], - ) - - await callback_query.message.answer( - f"Вы уверены, что хотите удалить пользователя с ID {tg_id}?", - reply_markup=confirmation_markup, - ) - - -@router.callback_query(F.data.startswith("delete_user_"), IsAdminFilter()) -async def delete_user(callback_query: types.CallbackQuery, session: Any): - tg_id = int(callback_query.data.split("_")[2]) - - key_records = await session.fetch("SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id) - - async def delete_keys_from_servers(): - try: - tasks = [] - for email, client_id in key_records: - servers = await get_servers(session) - for cluster_id, _cluster in servers.items(): - tasks.append(delete_key_from_cluster(cluster_id, email, client_id)) - await asyncio.gather(*tasks) - except Exception as e: - logger.error(f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}") - - await delete_keys_from_servers() - - try: - await delete_user_data(session, tg_id) - - back_button = InlineKeyboardButton(text="⬅️ Назад", callback_data="user_editor") - keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]]) - - await callback_query.message.answer(f"🗑️ Пользователь с ID {tg_id} был удален.", reply_markup=keyboard) - except Exception as e: - logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}") - await callback_query.message.answer( - f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова." - ) From 3de20a573481d2999eae85c12989ab733b6a08a6 Mon Sep 17 00:00:00 2001 From: hteppl Date: Fri, 31 Jan 2025 06:45:56 +0300 Subject: [PATCH 7/8] Missing doc for field in update_key_expiry --- database.py | 1 + 1 file changed, 1 insertion(+) diff --git a/database.py b/database.py index ea1b5cab..ff56aa79 100644 --- a/database.py +++ b/database.py @@ -819,6 +819,7 @@ async def update_key_expiry(client_id: str, new_expiry_time: int, session: Any): Args: client_id (str): Уникальный идентификатор клиента new_expiry_time (int): Новое время истечения ключа + session (Any): Сессия подключения к базе данных Raises: Exception: В случае ошибки при подключении к базе данных или обновлении ключа From 419e8a6597477238d1f2b38cadd74740396b2119 Mon Sep 17 00:00:00 2001 From: hteppl Date: Fri, 31 Jan 2025 06:46:53 +0300 Subject: [PATCH 8/8] Temporary fix for key expiry change --- handlers/admin/admin_users.py | 75 +++++++++++++++-------------------- keyboards/admin/users_kb.py | 1 + 2 files changed, 33 insertions(+), 43 deletions(-) diff --git a/handlers/admin/admin_users.py b/handlers/admin/admin_users.py index 8ad42bea..fea31df4 100644 --- a/handlers/admin/admin_users.py +++ b/handlers/admin/admin_users.py @@ -4,13 +4,13 @@ from typing import Any from aiogram import F, Router, types from aiogram.exceptions import TelegramBadRequest -from aiogram.filters.callback_data import CallbackData from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery from config import TOTAL_GB -from database import delete_key, delete_user_data, get_client_id_by_email, get_servers, update_key_expiry, update_trial +from database import delete_key, delete_user_data, get_client_id_by_email, get_servers, update_key_expiry, update_trial, \ + get_balance, update_balance from filters.admin import IsAdminFilter from handlers.keys.key_utils import ( delete_key_from_cluster, @@ -132,12 +132,13 @@ async def handle_key_name_input(message: types.Message, state: FSMContext, sessi IsAdminFilter(), ) async def handle_send_message( - callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext + callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext ): tg_id = callback_data.tg_id await callback_query.message.edit_text( - text="✉️ Введите текст сообщения, которое вы хотите отправить пользователю:", reply_markup=build_editor_kb(tg_id) + text="✉️ Введите текст сообщения, которое вы хотите отправить пользователю:", + reply_markup=build_editor_kb(tg_id) ) await state.update_data(tg_id=tg_id) @@ -163,7 +164,7 @@ async def handle_message_text_input(message: types.Message, state: FSMContext): IsAdminFilter(), ) async def handle_trial_restore( - callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any ): tg_id = callback_data.tg_id @@ -186,7 +187,7 @@ async def handle_balance_change(callback_query: CallbackQuery, callback_data: Ad tg_id, ) - balance = await get_user_balance(tg_id, session) + balance = await get_balance(tg_id) text = ( f"💵 Изменение баланса" @@ -214,13 +215,13 @@ async def handle_balance_change(callback_query: CallbackQuery, callback_data: Ad @router.callback_query(AdminUserEditorCallback.filter(F.action == "users_balance_add"), IsAdminFilter()) async def handle_balance_add( - callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any + callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any ): tg_id = callback_data.tg_id amount = callback_data.data if amount: - await add_user_balance(tg_id, int(amount), session) + await update_balance(tg_id, int(amount), session) await handle_balance_change(callback_query, callback_data, session) return @@ -275,10 +276,10 @@ async def handle_balance_input(message: types.Message, state: FSMContext, sessio if op_type == "add": text = f"✅ К балансу пользователя добавлено {amount}Р" - await add_user_balance(tg_id, amount, session) + await update_balance(tg_id, amount, session) elif op_type == "take": text = f"✅ Из баланса пользователя было вычтено {amount}Р" - await add_user_balance(tg_id, -amount, session) + await update_balance(tg_id, -amount, session) else: text = f"✅ Баланс пользователя изменен на {amount}Р" await set_user_balance(tg_id, amount, session) @@ -288,7 +289,8 @@ async def handle_balance_input(message: types.Message, state: FSMContext, sessio @router.callback_query(AdminUserEditorCallback.filter(F.action == "users_key_edit"), IsAdminFilter()) async def handle_key_edit( - callback_query: CallbackQuery, callback_data: CallbackData, session: Any, update: bool = False + callback_query: CallbackQuery, callback_data: AdminUserEditorCallback | AdminUserKeyEditorCallback, + session: Any, update: bool = False ): email = callback_data.data key_details = await get_key_details(email, session) @@ -326,7 +328,7 @@ async def handle_change_expiry(callback_query: CallbackQuery, callback_data: Adm @router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "add"), IsAdminFilter()) async def handle_expiry_add( - callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext, session: Any + callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext, session: Any ): tg_id = callback_data.tg_id email = callback_data.data @@ -357,7 +359,7 @@ async def handle_expiry_add( @router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "take"), IsAdminFilter()) async def handle_expiry_take( - callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext + callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext ): tg_id = callback_data.tg_id email = callback_data.data @@ -373,18 +375,27 @@ async def handle_expiry_take( @router.callback_query(AdminUserKeyEditorCallback.filter(F.action == "set"), IsAdminFilter()) async def handle_expiry_set( - callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext + callback_query: CallbackQuery, callback_data: AdminUserKeyEditorCallback, state: FSMContext, session: Any ): tg_id = callback_data.tg_id email = callback_data.data + key_details = await get_key_details(email, session) + + if not key_details: + await callback_query.message.edit_text( + text="🚫 Информация о ключе не найдена.", + reply_markup=build_editor_kb(tg_id), + ) + return + await state.update_data(tg_id=tg_id, email=email, op_type="set") await state.set_state(UserEditorState.waiting_for_expiry_time) text = ( "✍️ Введите новое время действия ключа:" "\n\n📌 Формат: год-месяц-день час:минута" - "\n Пример: 2025-02-09 09:01" + f"\n\n📄 Текущая дата: {datetime.fromtimestamp(key_details['expiry_time'] / 1000).strftime('%Y-%m-%d %H:%M')}" ) await callback_query.message.edit_text(text=text, reply_markup=build_users_key_show_kb(tg_id, email)) @@ -441,7 +452,7 @@ async def handle_update_key(callback_query: CallbackQuery, callback_data: AdminU try: await update_subscription(tg_id, email, session) - await handle_key_edit(callback_query, callback_data, session) + await handle_key_edit(callback_query, callback_data, session, True) except TelegramBadRequest: pass except Exception as e: @@ -469,7 +480,7 @@ async def handle_delete_key(callback_query: types.CallbackQuery, callback_data: @router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_key_confirm"), IsAdminFilter()) async def handle_delete_key_confirm( - callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any ): email = callback_data.data record = await session.fetchrow("SELECT client_id FROM keys WHERE email = $1", email) @@ -505,7 +516,7 @@ async def handle_delete_user(callback_query: types.CallbackQuery, callback_data: @router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_user_confirm"), IsAdminFilter()) async def handle_delete_user_confirm( - callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any + callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any ): tg_id = callback_data.tg_id key_records = await session.fetch("SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id) @@ -537,13 +548,13 @@ async def handle_delete_user_confirm( @router.callback_query(AdminUserEditorCallback.filter(F.action == "users_editor"), IsAdminFilter()) async def handle_editor( - callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any + callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any ): await process_user_search(callback_query.message, state, session, callback_data.tg_id, callback_data.edit) async def process_user_search( - message: types.Message, state: FSMContext, session: Any, tg_id: int, edit: bool = False + message: types.Message, state: FSMContext, session: Any, tg_id: int, edit: bool = False ) -> None: await state.clear() @@ -637,29 +648,7 @@ async def change_expiry_time(expiry_time: int, email: str, session: Any) -> Exce await asyncio.gather(*tasks) await update_key_on_all_servers() - await update_key_expiry(client_id, expiry_time) - - -async def get_user_balance(tg_id: int, session: Any) -> float: - try: - return await session.fetchval( - "SELECT balance FROM connections WHERE tg_id = $1", - tg_id, - ) - except Exception as e: - logger.error(f"Ошибка при получении баланса для пользователя {tg_id}: {e}") - return -1 - - -async def add_user_balance(tg_id: int, balance: int, session: Any) -> None: - try: - await session.execute( - "UPDATE connections SET balance = balance + $1 WHERE tg_id = $2", - balance, - tg_id, - ) - except Exception as e: - logger.error(f"Ошибка при добавлении баланса для пользователя {tg_id}: {e}") + await update_key_expiry(client_id, expiry_time, session) async def set_user_balance(tg_id: int, balance: int, session: Any) -> None: diff --git a/keyboards/admin/users_kb.py b/keyboards/admin/users_kb.py index 54d056a9..ae13f2ea 100644 --- a/keyboards/admin/users_kb.py +++ b/keyboards/admin/users_kb.py @@ -20,6 +20,7 @@ class AdminUserKeyEditorCallback(CallbackData, prefix="admin_users_key"): tg_id: int data: str month: int | None = None + edit: bool = False def build_user_edit_kb(tg_id: int, key_records: list) -> InlineKeyboardMarkup: