Merge remote-tracking branch 'origin/fix/admin-actions'
This commit is contained in:
@@ -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: В случае ошибки при подключении к базе данных или обновлении ключа
|
||||
|
||||
@@ -8,7 +8,7 @@ from aiogram.types import CallbackQuery, Message
|
||||
|
||||
from filters.admin import IsAdminFilter
|
||||
from keyboards.admin.panel_kb import AdminPanelCallback, build_admin_back_kb
|
||||
from keyboards.admin.sender_kb import AdminSenderCallback, build_sender_kb
|
||||
from keyboards.admin.sender_kb import AdminSenderCallback, build_sender_kb, build_clusters_kb
|
||||
from logger import logger
|
||||
|
||||
router = Router()
|
||||
@@ -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,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, cluster_name=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: Message, state: FSMContext, session: Any
|
||||
""",
|
||||
int(datetime.utcnow().timestamp() * 1000),
|
||||
)
|
||||
elif send_to == "cluster":
|
||||
cluster_name = state_data.get("cluster_name")
|
||||
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")
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
import asyncpg
|
||||
@@ -11,6 +12,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,
|
||||
@@ -19,6 +21,7 @@ from keyboards.admin.servers_kb import (
|
||||
build_manage_cluster_kb,
|
||||
build_manage_server_kb,
|
||||
)
|
||||
from logger import logger
|
||||
|
||||
router = Router()
|
||||
|
||||
@@ -230,7 +233,9 @@ async def handle_inbound_id_input(message: Message, state: FSMContext):
|
||||
|
||||
|
||||
@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_manage"), IsAdminFilter())
|
||||
async def handle_clusters_manage(callback_query: CallbackQuery, callback_data: AdminServerEditorCallback, session: Any):
|
||||
async def handle_clusters_manage(
|
||||
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
|
||||
):
|
||||
cluster_name = callback_data.data
|
||||
|
||||
servers = await get_servers(session)
|
||||
@@ -244,7 +249,7 @@ async def handle_clusters_manage(callback_query: CallbackQuery, callback_data: A
|
||||
|
||||
@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_availability"), IsAdminFilter())
|
||||
async def handle_servers_availability(
|
||||
callback_query: CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
|
||||
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
|
||||
):
|
||||
cluster_name = callback_data.data
|
||||
|
||||
@@ -320,7 +325,7 @@ async def handle_servers_delete(callback_query: CallbackQuery, callback_data: Ad
|
||||
|
||||
@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_delete_confirm"), IsAdminFilter())
|
||||
async def handle_servers_delete_confirm(
|
||||
callback_query: CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
|
||||
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
|
||||
):
|
||||
server_name = callback_data.data
|
||||
|
||||
@@ -333,7 +338,7 @@ async def handle_servers_delete_confirm(
|
||||
|
||||
@router.callback_query(AdminServerEditorCallback.filter(F.action == "servers_add"), IsAdminFilter())
|
||||
async def handle_servers_add(
|
||||
callback_query: CallbackQuery, callback_data: AdminServerEditorCallback, state: FSMContext
|
||||
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, state: FSMContext
|
||||
):
|
||||
cluster_name = callback_data.data
|
||||
|
||||
@@ -354,7 +359,9 @@ async def handle_servers_add(
|
||||
|
||||
|
||||
@router.callback_query(AdminServerEditorCallback.filter(F.action == "clusters_backup"), IsAdminFilter())
|
||||
async def handle_clusters_backup(callback_query: CallbackQuery, callback_data: AdminServerEditorCallback, session: Any):
|
||||
async def handle_clusters_backup(
|
||||
callback_query: types.CallbackQuery, callback_data: AdminServerEditorCallback, session: Any
|
||||
):
|
||||
cluster_name = callback_data.data
|
||||
|
||||
servers = await get_servers(session)
|
||||
@@ -377,3 +384,53 @@ async def handle_clusters_backup(callback_query: CallbackQuery, callback_data: A
|
||||
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")
|
||||
)
|
||||
|
||||
@@ -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, Message
|
||||
|
||||
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,
|
||||
@@ -131,11 +131,14 @@ async def handle_key_name_input(message: Message, state: FSMContext, session: An
|
||||
AdminUserEditorCallback.filter(F.action == "users_send_message"),
|
||||
IsAdminFilter(),
|
||||
)
|
||||
async def handle_send_message(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext):
|
||||
async def handle_send_message(
|
||||
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)
|
||||
@@ -160,7 +163,9 @@ async def handle_message_text_input(message: Message, state: FSMContext):
|
||||
AdminUserEditorCallback.filter(F.action == "users_trial_restore"),
|
||||
IsAdminFilter(),
|
||||
)
|
||||
async def handle_trial_restore(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, session: Any):
|
||||
async def handle_trial_restore(
|
||||
callback_query: types.CallbackQuery, callback_data: AdminUserEditorCallback, session: Any
|
||||
):
|
||||
tg_id = callback_data.tg_id
|
||||
|
||||
await update_trial(tg_id, 0, session)
|
||||
@@ -182,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"<b>💵 Изменение баланса</b>"
|
||||
@@ -210,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
|
||||
|
||||
@@ -271,10 +276,10 @@ async def handle_balance_input(message: Message, state: FSMContext, session: Any
|
||||
|
||||
if op_type == "add":
|
||||
text = f"✅ К балансу пользователя добавлено <b>{amount}Р</b>"
|
||||
await add_user_balance(tg_id, amount, session)
|
||||
await update_balance(tg_id, amount, session)
|
||||
elif op_type == "take":
|
||||
text = f"✅ Из баланса пользователя было вычтено <b>{amount}Р</b>"
|
||||
await add_user_balance(tg_id, -amount, session)
|
||||
await update_balance(tg_id, -amount, session)
|
||||
else:
|
||||
text = f"✅ Баланс пользователя изменен на <b>{amount}Р</b>"
|
||||
await set_user_balance(tg_id, amount, session)
|
||||
@@ -284,7 +289,8 @@ async def handle_balance_input(message: Message, state: FSMContext, session: Any
|
||||
|
||||
@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)
|
||||
@@ -322,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
|
||||
@@ -353,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
|
||||
@@ -369,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📌 Формат: <b>год-месяц-день час:минута</b>"
|
||||
"\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))
|
||||
@@ -437,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:
|
||||
@@ -465,7 +480,7 @@ async def handle_delete_key(callback_query: CallbackQuery, callback_data: AdminU
|
||||
|
||||
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_key_confirm"), IsAdminFilter())
|
||||
async def handle_delete_key_confirm(
|
||||
callback_query: 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)
|
||||
@@ -501,7 +516,7 @@ async def handle_delete_user(callback_query: CallbackQuery, callback_data: Admin
|
||||
|
||||
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_delete_user_confirm"), IsAdminFilter())
|
||||
async def handle_delete_user_confirm(
|
||||
callback_query: 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)
|
||||
@@ -533,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: 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: 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()
|
||||
|
||||
@@ -636,28 +651,6 @@ async def change_expiry_time(expiry_time: int, email: str, session: Any) -> Exce
|
||||
await update_key_expiry(client_id, expiry_time, session)
|
||||
|
||||
|
||||
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}")
|
||||
|
||||
|
||||
async def set_user_balance(tg_id: int, balance: int, session: Any) -> None:
|
||||
try:
|
||||
await session.execute(
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from keyboards.admin.panel_kb import build_admin_back_btn
|
||||
|
||||
class AdminSenderCallback(CallbackData, prefix="admin_sender"):
|
||||
type: str
|
||||
data: str | None = None
|
||||
|
||||
|
||||
def build_sender_kb() -> InlineKeyboardMarkup:
|
||||
@@ -14,6 +15,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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user