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,
]