diff --git a/handlers/admin/admin_users.py b/handlers/admin/admin_users.py index b118bdfc..44b62f7b 100644 --- a/handlers/admin/admin_users.py +++ b/handlers/admin/admin_users.py @@ -1,10 +1,11 @@ import asyncio -from datetime import datetime +from datetime import datetime, UTC from typing import Any import pytz 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 @@ -21,7 +22,8 @@ from handlers.utils import sanitize_key_name from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb from keyboards.admin.users_kb import build_user_edit_kb, build_key_edit_kb, build_key_delete_kb, \ build_user_delete_kb, AdminUserEditorCallback, build_editor_kb, build_users_balance_kb, \ - build_users_balance_change_kb, build_user_key_kb + build_users_balance_change_kb, build_user_key_kb, build_users_key_expiry_kb, AdminUserKeyEditorCallback, \ + build_users_key_show_kb from logger import logger router = Router() @@ -299,7 +301,7 @@ async def handle_balance_add( AdminUserEditorCallback.filter(F.action == "users_balance_take"), IsAdminFilter() ) -async def handle_balance_add( +async def handle_balance_take( callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext @@ -319,7 +321,7 @@ async def handle_balance_add( AdminUserEditorCallback.filter(F.action == "users_balance_set"), IsAdminFilter() ) -async def handle_balance_add( +async def handle_balance_set( callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext @@ -379,8 +381,9 @@ async def handle_balance_input( ) async def handle_key_edit( callback_query: CallbackQuery, - callback_data: AdminUserEditorCallback, - session: Any + callback_data: CallbackData, + session: Any, + update: bool = False ): email = callback_data.data key_details = await get_key_details(email, session) @@ -395,35 +398,179 @@ async def handle_key_edit( text = ( f"🔑 Информация о ключе" f"\n\n{key_details['key']}" - f"\n\n⏰ Дата истечения: {key_details['expiry_date']}" + f"\n\n⏰ Дата истечения: {key_details['expiry_date']} (UTC)" f"\n🌐 Кластер: {key_details['cluster_name']}" f"\n🆔 ID клиента: {key_details['tg_id']}" ) - await callback_query.message.edit_text( - text=text, - reply_markup=build_key_edit_kb(key_details, email) - ) + if not update or not callback_data.edit: + await callback_query.message.edit_text( + text=text, + reply_markup=build_key_edit_kb(key_details, email) + ) + else: + await callback_query.message.edit_text( + text=text, + reply_markup=build_users_key_expiry_kb(callback_data.tg_id, email) + ) @router.callback_query( - AdminUserEditorCallback.filter(F.action == "users_change_expiry"), + AdminUserEditorCallback.filter(F.action == "users_expiry_edit"), IsAdminFilter() ) async def handle_change_expiry( callback_query: CallbackQuery, - callback_data: AdminUserEditorCallback, - state: FSMContext + callback_data: AdminUserEditorCallback ): + tg_id = callback_data.tg_id email = callback_data.data - await callback_query.message.edit_text( - text=f"✍️ Введите новое время истечения для ключа {email} в формате YYYY-MM-DD HH:MM:SS:" + await callback_query.message.edit_reply_markup( + reply_markup=build_users_key_expiry_kb(tg_id, email) ) - await state.update_data(tg_id=callback_data.tg_id, email=email) + +@router.callback_query( + AdminUserKeyEditorCallback.filter(F.action == "add"), + IsAdminFilter() +) +async def handle_expiry_add( + callback_query: CallbackQuery, + callback_data: AdminUserKeyEditorCallback, + state: FSMContext, + session: Any +): + tg_id = callback_data.tg_id + email = callback_data.data + month = callback_data.month + + 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 + + if month: + await change_expiry_time(key_details["expiry_time"] + month * 30 * 24 * 3600 * 1000, email, session) + await handle_key_edit(callback_query, callback_data, session, True) + return + + await state.update_data(tg_id=tg_id, email=email, op_type="add") await state.set_state(UserEditorState.waiting_for_expiry_time) + await callback_query.message.edit_text( + text="✍️ Введите количество дней, которое хотите добавить к времени действия ключа:", + reply_markup=build_users_key_show_kb(tg_id, email) + ) + + +@router.callback_query( + AdminUserKeyEditorCallback.filter(F.action == "take"), + IsAdminFilter() +) +async def handle_expiry_take( + callback_query: CallbackQuery, + callback_data: AdminUserKeyEditorCallback, + state: FSMContext +): + tg_id = callback_data.tg_id + email = callback_data.data + + await state.update_data(tg_id=tg_id, email=email, op_type="take") + await state.set_state(UserEditorState.waiting_for_expiry_time) + + await callback_query.message.edit_text( + text="✍️ Введите количество дней, которое хотите вычесть из времени действия ключа:", + reply_markup=build_users_key_show_kb(tg_id, email) + ) + + +@router.callback_query( + AdminUserKeyEditorCallback.filter(F.action == "set"), + IsAdminFilter() +) +async def handle_expiry_set( + callback_query: CallbackQuery, + callback_data: AdminUserKeyEditorCallback, + state: FSMContext +): + tg_id = callback_data.tg_id + email = callback_data.data + + 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" + ) + + await callback_query.message.edit_text( + text=text, + reply_markup=build_users_key_show_kb(tg_id, email) + ) + + +@router.message( + UserEditorState.waiting_for_expiry_time, + IsAdminFilter() +) +async def handle_expiry_time_input( + message: types.Message, + state: FSMContext, + session: Any +): + data = await state.get_data() + tg_id = data.get("tg_id") + email = data.get("email") + op_type = data.get("op_type") + + if op_type != "set" and (not message.text.isdigit() or int(message.text) < 0): + await message.answer( + text="🚫 Пожалуйста, введите корректное количество дней!", + reply_markup=build_users_key_show_kb(tg_id, email) + ) + return + + key_details = await get_key_details(email, session) + + if not key_details: + await message.answer( + text="🚫 Информация о ключе не найдена.", + reply_markup=build_editor_kb(tg_id), + ) + return + + if op_type == "add": + days = int(message.text) + text = f"✅ Ко времени действия ключа добавлено {days} дн." + await change_expiry_time(key_details["expiry_time"] + days * 24 * 3600 * 1000, email, session) + elif op_type == "take": + days = int(message.text) + text = f"✅ Из времени действия ключа вычтено {days} дн." + await change_expiry_time(key_details["expiry_time"] - days * 24 * 3600 * 1000, email, session) + else: + try: + expiry_time = int( + datetime.strptime(message.text, "%Y-%m-%d %H:%M").timestamp() * 1000 + ) + text = f"✅ Время действия ключа изменено на {message.text}" + await change_expiry_time(expiry_time, email, session) + except ValueError: + text = f"🚫 Пожалуйста, используйте корректный формат даты!" + except Exception as e: + text = f"❗ Произошла ошибка во время изменения времени действия ключа: {e}" + + await message.answer( + text=text, + reply_markup=build_users_key_show_kb(tg_id, email) + ) + @router.message( UserEditorState.waiting_for_expiry_time, @@ -438,54 +585,8 @@ async def handle_expiry_time_input( email = user_data.get("email") try: - expiry_time = int( - datetime.strptime(message.text, "%Y-%m-%d %H:%M:%S").timestamp() * 1000 - ) - client_id = await get_client_id_by_email(email) - - if client_id is None: - await message.edit_text( - text=f"🚫 Клиент с Email {email} не найден. 🔍", - reply_markup=build_admin_back_kb(), - ) - await state.clear() - return - - server_id = await session.fetchrow( - "SELECT server_id FROM keys WHERE client_id = $1", client_id - ) - - if not server_id: - await message.edit_text( - text="🚫 Клиент не найден в базе данных. 🔍", - reply_markup=build_admin_back_kb(), - ) - await state.clear() - return - - clusters = await get_servers_from_db() - - async def update_key_on_all_servers(): - tasks = [ - asyncio.create_task( - renew_key_in_cluster( - cluster_name, - email, - client_id, - expiry_time, - total_gb=TOTAL_GB, - ) - ) - for cluster_name in clusters - ] - - await asyncio.gather(*tasks) - - await update_key_on_all_servers() - await update_key_expiry(client_id, expiry_time) - - response_message = f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах." + response_message = f"✅ Время истечения ключа для клиента ({email}) успешно обновлено на всех серверах." await message.edit_text( text=response_message, @@ -739,8 +840,7 @@ async def get_key_details(email, session): return None cluster_name = record["server_id"] - moscow_tz = pytz.timezone("Europe/Moscow") - expiry_date = datetime.fromtimestamp(record["expiry_time"] / 1000, tz=moscow_tz) + expiry_date = datetime.fromtimestamp(record["expiry_time"] / 1000, tz=UTC) return { "client_id": record["client_id"], @@ -748,10 +848,46 @@ async def get_key_details(email, session): "tg_id": record["tg_id"], "key": record["key"], "cluster_name": cluster_name, + "expiry_time": record["expiry_time"], "expiry_date": expiry_date.strftime("%d %B %Y года %H:%M"), } +async def change_expiry_time(expiry_time: int, email: str, session: Any) -> Exception | None: + client_id = await get_client_id_by_email(email) + + if client_id is None: + return ValueError(f"User with email {email} was not found") + + server_id = await session.fetchrow( + "SELECT server_id FROM keys WHERE client_id = $1", client_id + ) + + if not server_id: + return ValueError(f"User with client_id {server_id} was not found") + + clusters = await get_servers_from_db() + + async def update_key_on_all_servers(): + tasks = [ + asyncio.create_task( + renew_key_in_cluster( + cluster_name, + email, + client_id, + expiry_time, + total_gb=TOTAL_GB, + ) + ) + for cluster_name in clusters + ] + + 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( diff --git a/keyboards/admin/users_kb.py b/keyboards/admin/users_kb.py index b71a3547..d301605c 100644 --- a/keyboards/admin/users_kb.py +++ b/keyboards/admin/users_kb.py @@ -13,6 +13,13 @@ class AdminUserEditorCallback(CallbackData, prefix="admin_users"): edit: bool = False +class AdminUserKeyEditorCallback(CallbackData, prefix="admin_users_key"): + action: str + tg_id: int + data: str + month: int | None = None + + def build_user_edit_kb(tg_id: int, key_records: list) -> InlineKeyboardMarkup: builder = InlineKeyboardBuilder() @@ -124,6 +131,78 @@ def build_users_balance_kb(tg_id: int) -> InlineKeyboardMarkup: return builder.as_markup() +def build_users_key_show_kb(tg_id: int, email: str) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + builder.button( + text="🔙 Назад", # todo: fix magic text was set + callback_data=AdminUserEditorCallback( + action="users_key_edit", + tg_id=tg_id, + data=email, + edit=True + ).pack() + ) + return builder.as_markup() + + +def build_users_key_expiry_kb(tg_id: int, email: str) -> InlineKeyboardMarkup: + builder = InlineKeyboardBuilder() + for month in RENEWAL_PRICES.keys(): + month = int(month) + builder.button( + text=f"+ {month} мес.", + callback_data=AdminUserKeyEditorCallback( + action="add", + tg_id=tg_id, + data=email, + month=month + ).pack() + ) + builder.button( + text=f"- {month} мес.", + callback_data=AdminUserKeyEditorCallback( + action="add", + tg_id=tg_id, + data=email, + month=-month + ).pack() + ) + builder.button( + text="⏳ Добавить дни", + callback_data=AdminUserKeyEditorCallback( + action="add", + tg_id=tg_id, + data=email + ).pack() + ) + builder.button( + text="⏳ Вычесть дни", + callback_data=AdminUserKeyEditorCallback( + action="take", + tg_id=tg_id, + data=email + ).pack() + ) + builder.button( + text="⏳ Установить дату истечения", + callback_data=AdminUserKeyEditorCallback( + action="set", + tg_id=tg_id, + data=email + ).pack() + ) + builder.button( + text="🔙 Назад", # todo: fix magic text was set + callback_data=AdminUserEditorCallback( + action="users_key_edit", + tg_id=tg_id, + data=email + ).pack() + ) + builder.adjust(2, 2, 2, 2, 2, 1) + return builder.as_markup() + + def build_user_delete_kb(tg_id: int): builder = InlineKeyboardBuilder() builder.button( @@ -159,7 +238,7 @@ def build_key_edit_kb(key_details: dict, email: str) -> InlineKeyboardMarkup: builder.button( text="⏳ Время истечения", callback_data=AdminUserEditorCallback( - action="users_change_expiry", + action="users_expiry_edit", data=email, tg_id=key_details["tg_id"] ).pack() diff --git a/middlewares/delete.py b/middlewares/delete.py index b3ccdd37..29281507 100644 --- a/middlewares/delete.py +++ b/middlewares/delete.py @@ -8,7 +8,7 @@ from keyboards.admin.coupons_kb import AdminCouponDeleteCallback from keyboards.admin.panel_kb import AdminPanelCallback from keyboards.admin.sender_kb import AdminSenderCallback from keyboards.admin.servers_kb import AdminServerEditorCallback -from keyboards.admin.users_kb import AdminUserEditorCallback +from keyboards.admin.users_kb import AdminUserEditorCallback, AdminUserKeyEditorCallback pass_callbacks = [ AdminPanelCallback, @@ -16,6 +16,7 @@ pass_callbacks = [ AdminSenderCallback, AdminServerEditorCallback, AdminUserEditorCallback, + AdminUserKeyEditorCallback, ]