Initial commit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,513 @@
|
||||
"""Обработчики админ-панели для управления пользователями."""
|
||||
|
||||
import logging
|
||||
import math
|
||||
from datetime import datetime, timedelta
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import CallbackQuery, Message, InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.filters import Command
|
||||
|
||||
from config import settings
|
||||
from database.database import db
|
||||
|
||||
router = Router()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Константы для пагинации
|
||||
USERS_PER_PAGE = 10
|
||||
|
||||
|
||||
class AddUserState(StatesGroup):
|
||||
"""Состояния для процесса добавления пользователя."""
|
||||
waiting_for_user_id = State()
|
||||
waiting_for_access_type = State() # unlimited, time, generations
|
||||
waiting_for_time_days = State()
|
||||
waiting_for_generations_limit = State()
|
||||
|
||||
|
||||
def is_admin(user_id: int) -> bool:
|
||||
"""Проверить, является ли пользователь админом."""
|
||||
return user_id == settings.ADMIN_ID
|
||||
|
||||
|
||||
def check_admin(handler):
|
||||
"""Декоратор для проверки прав админа."""
|
||||
async def wrapper(callback_or_message, *args, **kwargs):
|
||||
# Фильтруем лишние kwargs, которые не ожидаются хендлером
|
||||
# Оставляем только state и другие FSM-аргументы
|
||||
allowed_kwargs = {'state', 'session', 'middleware_data'}
|
||||
filtered_kwargs = {k: v for k, v in kwargs.items() if k in allowed_kwargs}
|
||||
|
||||
if isinstance(callback_or_message, CallbackQuery):
|
||||
user_id = callback_or_message.from_user.id
|
||||
elif isinstance(callback_or_message, Message):
|
||||
user_id = callback_or_message.from_user.id
|
||||
else:
|
||||
return await callback_or_message.answer("⛔ Ошибка определения пользователя.")
|
||||
|
||||
if not is_admin(user_id):
|
||||
if isinstance(callback_or_message, CallbackQuery):
|
||||
await callback_or_message.answer("⛔ Недостаточно прав.", show_alert=True)
|
||||
else:
|
||||
await callback_or_message.answer("⛔ Недостаточно прав.")
|
||||
return None
|
||||
return await handler(callback_or_message, *args, **filtered_kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
@router.message(Command("admin"))
|
||||
@check_admin
|
||||
async def cmd_admin(message: Message, state: FSMContext):
|
||||
"""Админ-панель."""
|
||||
await state.clear()
|
||||
await show_admin_panel(message)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin_panel")
|
||||
@check_admin
|
||||
async def admin_panel(callback: CallbackQuery, state: FSMContext):
|
||||
"""Показать админ-панель."""
|
||||
await state.clear()
|
||||
await show_admin_panel(callback.message)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
async def show_admin_panel(target):
|
||||
"""Отобразить админ-панель."""
|
||||
all_users = await db.get_all_users()
|
||||
active_users = [u for u in all_users if u["is_active"]]
|
||||
total_gens = sum(u.get("used_generations", 0) for u in all_users)
|
||||
|
||||
text = (
|
||||
"🛡️ <b>Админ-панель</b>\n\n"
|
||||
f"👥 Всего пользователей: <b>{len(all_users)}</b>\n"
|
||||
f"✅ Активных: <b>{len(active_users)}</b>\n"
|
||||
f"🎨 Всего генераций: <b>{total_gens}</b>\n\n"
|
||||
"<b>Управление:</b>"
|
||||
)
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="➕ Добавить пользователя", callback_data="admin_add_user")],
|
||||
[InlineKeyboardButton(text="📋 Список пользователей", callback_data="admin_users_list")],
|
||||
[InlineKeyboardButton(text="🧹 Очистить истёкшие доступы", callback_data="admin_cleanup_expired")],
|
||||
[InlineKeyboardButton(text="⬅️ Назад", callback_data="main_menu")],
|
||||
])
|
||||
|
||||
if hasattr(target, 'answer'):
|
||||
try:
|
||||
await target.edit_text(text, parse_mode="HTML", reply_markup=keyboard)
|
||||
except Exception:
|
||||
await target.answer(text, parse_mode="HTML", reply_markup=keyboard)
|
||||
else:
|
||||
await target.answer(text, parse_mode="HTML", reply_markup=keyboard)
|
||||
|
||||
|
||||
# --- Добавление пользователя ---
|
||||
|
||||
@router.callback_query(F.data == "admin_add_user")
|
||||
@check_admin
|
||||
async def admin_add_user(callback: CallbackQuery, state: FSMContext):
|
||||
"""Начать процесс добавления пользователя."""
|
||||
await state.set_state(AddUserState.waiting_for_user_id)
|
||||
await callback.message.edit_text(
|
||||
"➕ <b>Добавление нового пользователя</b>\n\n"
|
||||
"Введите <b>Telegram User ID</b> пользователя.\n\n"
|
||||
"💡 <i>Чтобы узнать ID, пользователь может отправить "
|
||||
"команду /start боту @userinfobot</i>\n\n"
|
||||
"Используйте <code>/cancel</code> для отмены.",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.message(AddUserState.waiting_for_user_id)
|
||||
@check_admin
|
||||
async def process_user_id(message: Message, state: FSMContext):
|
||||
"""Обработка введённого User ID."""
|
||||
text = message.text.strip()
|
||||
if not text.isdigit():
|
||||
await message.answer("❌ Введите корректный числовой ID.")
|
||||
return
|
||||
|
||||
user_id = int(text)
|
||||
|
||||
# Проверяем, не админ ли это
|
||||
if user_id == settings.ADMIN_ID:
|
||||
await message.answer("❌ Нельзя добавить самого себя как обычного пользователя.")
|
||||
return
|
||||
|
||||
# Проверяем, существует ли уже пользователь
|
||||
existing = await db.get_user_by_id(user_id)
|
||||
if existing:
|
||||
status_text = "активен" if existing["is_active"] else "неактивен"
|
||||
await message.answer(
|
||||
f"⚠️ Пользователь <code>{user_id}</code> уже существует в базе.\n"
|
||||
f"Статус: {status_text}\n"
|
||||
f"Его параметры будут обновлены.\n\n"
|
||||
"Продолжить? (да/нет)"
|
||||
)
|
||||
# Сохраняем и переходим к выбору типа доступа
|
||||
await state.update_data(user_id=user_id, existing_user=True)
|
||||
else:
|
||||
await state.update_data(user_id=user_id, existing_user=False)
|
||||
|
||||
await state.set_state(AddUserState.waiting_for_access_type)
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="♾️ Без ограничений", callback_data="access_unlimited")],
|
||||
[InlineKeyboardButton(text="⏰ По времени (дни)", callback_data="access_time")],
|
||||
[InlineKeyboardButton(text="🎨 По количеству генераций", callback_data="access_gens")],
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data="admin_panel")],
|
||||
])
|
||||
|
||||
await message.answer(
|
||||
"📋 <b>Выберите тип доступа:</b>",
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("access_"), AddUserState.waiting_for_access_type)
|
||||
@check_admin
|
||||
async def process_access_type(callback: CallbackQuery, state: FSMContext):
|
||||
"""Обработка выбора типа доступа."""
|
||||
access_type = callback.data.replace("access_", "")
|
||||
|
||||
if access_type == "unlimited":
|
||||
await state.update_data(access_type="unlimited", access_expires_at=None, max_generations=None)
|
||||
await _confirm_and_add_user(callback, state)
|
||||
|
||||
elif access_type == "time":
|
||||
await state.set_state(AddUserState.waiting_for_time_days)
|
||||
await state.update_data(access_type="time")
|
||||
await callback.message.edit_text(
|
||||
"⏰ <b>Введите количество дней</b> доступа:\n\n"
|
||||
"Например: 7, 30, 90, 365\n"
|
||||
"Используйте <code>/cancel</code> для отмены.",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
elif access_type == "gens":
|
||||
await state.set_state(AddUserState.waiting_for_generations_limit)
|
||||
await state.update_data(access_type="gens")
|
||||
await callback.message.edit_text(
|
||||
"🎨 <b>Введите максимальное количество генераций:</b>\n\n"
|
||||
"Например: 10, 50, 100\n"
|
||||
"Используйте <code>/cancel</code> для отмены.",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.message(AddUserState.waiting_for_time_days)
|
||||
@check_admin
|
||||
async def process_time_days(message: Message, state: FSMContext):
|
||||
"""Обработка введённого количества дней."""
|
||||
text = message.text.strip()
|
||||
if not text.isdigit():
|
||||
await message.answer("❌ Введите корректное число дней.")
|
||||
return
|
||||
|
||||
days = int(text)
|
||||
if days < 1 or days > 3650:
|
||||
await message.answer("❌ Количество дней должно быть от 1 до 3650.")
|
||||
return
|
||||
|
||||
expires_at = datetime.now() + timedelta(days=days)
|
||||
await state.update_data(access_expires_at=expires_at, max_generations=None)
|
||||
await _confirm_and_add_user(message, state)
|
||||
|
||||
|
||||
@router.message(AddUserState.waiting_for_generations_limit)
|
||||
@check_admin
|
||||
async def process_gens_limit(message: Message, state: FSMContext):
|
||||
"""Обработка введённого лимита генераций."""
|
||||
text = message.text.strip()
|
||||
if not text.isdigit():
|
||||
await message.answer("❌ Введите корректное число.")
|
||||
return
|
||||
|
||||
max_gens = int(text)
|
||||
if max_gens < 1 or max_gens > 100000:
|
||||
await message.answer("❌ Лимит должен быть от 1 до 100000.")
|
||||
return
|
||||
|
||||
await state.update_data(max_generations=max_gens, access_expires_at=None)
|
||||
await _confirm_and_add_user(message, state)
|
||||
|
||||
|
||||
async def _confirm_and_add_user(target, state: FSMContext):
|
||||
"""Подтвердить и добавить пользователя."""
|
||||
data = await state.get_data()
|
||||
user_id = data.get("user_id")
|
||||
|
||||
if user_id is None:
|
||||
await target.answer("❌ Ошибка: не указан User ID.")
|
||||
await state.clear()
|
||||
return
|
||||
|
||||
access_expires_at = data.get("access_expires_at")
|
||||
max_generations = data.get("max_generations")
|
||||
|
||||
# Добавляем/обновляем пользователя
|
||||
await db.add_user(
|
||||
user_id=user_id,
|
||||
added_by=target.from_user.id,
|
||||
access_expires_at=access_expires_at,
|
||||
max_generations=max_generations,
|
||||
)
|
||||
|
||||
# Формируем описание доступа
|
||||
if access_expires_at:
|
||||
expires_str = access_expires_at.strftime("%d.%m.%Y %H:%M")
|
||||
access_desc = f"⏰ До: <b>{expires_str}</b>"
|
||||
elif max_generations:
|
||||
access_desc = f"🎨 Лимит: <b>{max_generations}</b> генераций"
|
||||
else:
|
||||
access_desc = "♾️ Без ограничений"
|
||||
|
||||
text = (
|
||||
f"✅ <b>Пользователь <code>{user_id}</code> добавлен!</b>\n\n"
|
||||
f"{access_desc}"
|
||||
)
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="➕ Добавить ещё", callback_data="admin_add_user")],
|
||||
[InlineKeyboardButton(text="📋 Список пользователей", callback_data="admin_users_list")],
|
||||
[InlineKeyboardButton(text="🛡️ Админ-панель", callback_data="admin_panel")],
|
||||
])
|
||||
|
||||
if hasattr(target, 'edit_text'):
|
||||
try:
|
||||
await target.edit_text(text, parse_mode="HTML", reply_markup=keyboard)
|
||||
except Exception:
|
||||
await target.answer(text, parse_mode="HTML", reply_markup=keyboard)
|
||||
else:
|
||||
await target.answer(text, parse_mode="HTML", reply_markup=keyboard)
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
# --- Список пользователей ---
|
||||
|
||||
@router.callback_query(F.data == "admin_users_list")
|
||||
@check_admin
|
||||
async def admin_users_list(callback: CallbackQuery, state: FSMContext):
|
||||
"""Показать список пользователей."""
|
||||
await state.clear()
|
||||
await _show_users_list(callback.message, 0)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("admin_users_page_"))
|
||||
@check_admin
|
||||
async def admin_users_page(callback: CallbackQuery, state: FSMContext):
|
||||
"""Переключение страницы пользователей."""
|
||||
page = int(callback.data.split("_")[-1])
|
||||
await _show_users_list(callback.message, page)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
async def _show_users_list(target, page: int):
|
||||
"""Отобразить список пользователей."""
|
||||
all_users = await db.get_all_users()
|
||||
total_pages = math.ceil(len(all_users) / USERS_PER_PAGE) if all_users else 1
|
||||
|
||||
if page >= total_pages:
|
||||
page = total_pages - 1
|
||||
|
||||
start = page * USERS_PER_PAGE
|
||||
end = start + USERS_PER_PAGE
|
||||
page_users = all_users[start:end]
|
||||
|
||||
if not all_users:
|
||||
text = "📋 <b>Список пользователей пуст</b>"
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="➕ Добавить пользователя", callback_data="admin_add_user")],
|
||||
[InlineKeyboardButton(text="🛡️ Админ-панель", callback_data="admin_panel")],
|
||||
])
|
||||
else:
|
||||
text = f"👥 <b>Пользователи ({len(all_users)})</b>\n\n"
|
||||
for i, user in enumerate(page_users, start + 1):
|
||||
status = "👑" if user["is_admin"] else ("✅" if user["is_active"] else "❌")
|
||||
|
||||
# Формируем описание доступа
|
||||
if user["is_admin"]:
|
||||
access_info = "админ"
|
||||
elif user.get("access_expires_at"):
|
||||
try:
|
||||
exp = datetime.fromisoformat(user["access_expires_at"])
|
||||
if exp > datetime.now():
|
||||
days_left = (exp - datetime.now()).days
|
||||
access_info = f"до {exp.strftime('%d.%m.%Y')} ({days_left} дн.)"
|
||||
else:
|
||||
access_info = "истёк"
|
||||
except Exception:
|
||||
access_info = "—"
|
||||
elif user.get("max_generations"):
|
||||
used = user.get("used_generations", 0)
|
||||
access_info = f"{used}/{user['max_generations']} ген."
|
||||
else:
|
||||
access_info = "без ограничений"
|
||||
|
||||
name = user.get("display_name") or user.get("username") or f"ID: {user['user_id']}"
|
||||
text += f"{i}. {status} <code>{user['user_id']}</code> — {name}\n"
|
||||
text += f" {access_info}\n\n"
|
||||
|
||||
# Навигация
|
||||
nav_row = []
|
||||
if page > 0:
|
||||
nav_row.append(InlineKeyboardButton(text="⬅️", callback_data=f"admin_users_page_{page - 1}"))
|
||||
nav_row.append(InlineKeyboardButton(text=f"{page + 1}/{total_pages}", callback_data="noop"))
|
||||
if page < total_pages - 1:
|
||||
nav_row.append(InlineKeyboardButton(text="➡️", callback_data=f"admin_users_page_{page + 1}"))
|
||||
|
||||
kb_buttons = []
|
||||
for user in page_users:
|
||||
if not user["is_admin"]:
|
||||
kb_buttons.append([
|
||||
InlineKeyboardButton(
|
||||
text=f"{'✅' if user['is_active'] else '❌'} {user['user_id']}",
|
||||
callback_data=f"admin_user_manage_{user['user_id']}"
|
||||
)
|
||||
])
|
||||
|
||||
kb_buttons.append(nav_row)
|
||||
kb_buttons.append([InlineKeyboardButton(text="➕ Добавить", callback_data="admin_add_user")])
|
||||
kb_buttons.append([InlineKeyboardButton(text="🛡️ Админ-панель", callback_data="admin_panel")])
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=kb_buttons)
|
||||
|
||||
if hasattr(target, 'edit_text'):
|
||||
try:
|
||||
await target.edit_text(text, parse_mode="HTML", reply_markup=keyboard)
|
||||
except Exception:
|
||||
await target.answer(text, parse_mode="HTML", reply_markup=keyboard)
|
||||
else:
|
||||
await target.answer(text, parse_mode="HTML", reply_markup=keyboard)
|
||||
|
||||
|
||||
# --- Управление конкретным пользователем ---
|
||||
|
||||
@router.callback_query(F.data.startswith("admin_user_manage_"))
|
||||
@check_admin
|
||||
async def admin_user_manage(callback: CallbackQuery):
|
||||
"""Управление конкретным пользователем."""
|
||||
user_id = int(callback.data.split("_")[-1])
|
||||
user = await db.get_user_by_id(user_id)
|
||||
|
||||
if not user:
|
||||
await callback.answer("Пользователь не найден.", show_alert=True)
|
||||
return
|
||||
|
||||
# Формируем информацию
|
||||
status = "👑 Админ" if user["is_admin"] else ("✅ Активен" if user["is_active"] else "❌ Неактивен")
|
||||
|
||||
if user.get("access_expires_at"):
|
||||
try:
|
||||
exp = datetime.fromisoformat(user["access_expires_at"])
|
||||
expires_str = exp.strftime("%d.%m.%Y %H:%M")
|
||||
except Exception:
|
||||
expires_str = "—"
|
||||
else:
|
||||
expires_str = "не указан"
|
||||
|
||||
max_gens = user.get("max_generations") or "без ограничений"
|
||||
used_gens = user.get("used_generations", 0)
|
||||
added_by = user.get("added_by", "—")
|
||||
added_at = user.get("added_at", "—")
|
||||
|
||||
name = user.get("display_name") or user.get("username") or f"ID: {user_id}"
|
||||
|
||||
text = (
|
||||
f"👤 <b>{name}</b>\n\n"
|
||||
f"ID: <code>{user_id}</code>\n"
|
||||
f"Статус: {status}\n"
|
||||
f"Добавлен: {added_at} (админом {added_by})\n"
|
||||
f"Истекает: {expires_str}\n"
|
||||
f"Генерации: {used_gens}/{max_gens}"
|
||||
)
|
||||
|
||||
kb_buttons = []
|
||||
if not user["is_admin"]:
|
||||
if user["is_active"]:
|
||||
kb_buttons.append([InlineKeyboardButton(text="🚫 Заблокировать", callback_data=f"admin_user_block_{user_id}")])
|
||||
else:
|
||||
kb_buttons.append([InlineKeyboardButton(text="✅ Разблокировать", callback_data=f"admin_user_unblock_{user_id}")])
|
||||
kb_buttons.append([InlineKeyboardButton(text="🔄 Обновить доступ", callback_data=f"admin_user_update_{user_id}")])
|
||||
kb_buttons.append([InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"admin_user_delete_{user_id}")])
|
||||
|
||||
kb_buttons.append([InlineKeyboardButton(text="⬅️ Назад к списку", callback_data="admin_users_list")])
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=kb_buttons)
|
||||
|
||||
await callback.message.edit_text(text, parse_mode="HTML", reply_markup=keyboard)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("admin_user_block_"))
|
||||
@check_admin
|
||||
async def admin_user_block(callback: CallbackQuery):
|
||||
"""Заблокировать пользователя."""
|
||||
user_id = int(callback.data.split("_")[-1])
|
||||
await db.remove_user(user_id)
|
||||
await callback.answer("🚫 Пользователь заблокирован.")
|
||||
await admin_user_manage(callback)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("admin_user_unblock_"))
|
||||
@check_admin
|
||||
async def admin_user_unblock(callback: CallbackQuery):
|
||||
"""Разблокировать пользователя."""
|
||||
user_id = int(callback.data.split("_")[-1])
|
||||
await db.update_user_access(user_id)
|
||||
await callback.answer("✅ Пользователь разблокирован.")
|
||||
await admin_user_manage(callback)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("admin_user_delete_"))
|
||||
@check_admin
|
||||
async def admin_user_delete(callback: CallbackQuery):
|
||||
"""Удалить пользователя (окончательно)."""
|
||||
user_id = int(callback.data.split("_")[-1])
|
||||
# В текущей схеме просто деактивируем
|
||||
await db.remove_user(user_id)
|
||||
await callback.answer("🗑️ Пользователь удалён.")
|
||||
await admin_users_list(callback)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("admin_user_update_"))
|
||||
@check_admin
|
||||
async def admin_user_update(callback: CallbackQuery, state: FSMContext):
|
||||
"""Обновить параметры доступа пользователя."""
|
||||
user_id = int(callback.data.split("_")[-1])
|
||||
await state.update_data(update_user_id=user_id)
|
||||
await state.set_state(AddUserState.waiting_for_access_type)
|
||||
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="♾️ Без ограничений", callback_data="access_unlimited")],
|
||||
[InlineKeyboardButton(text="⏰ По времени (дни)", callback_data="access_time")],
|
||||
[InlineKeyboardButton(text="🎨 По количеству генераций", callback_data="access_gens")],
|
||||
[InlineKeyboardButton(text="❌ Отмена", callback_data=f"admin_user_manage_{user_id}")],
|
||||
])
|
||||
|
||||
await callback.message.edit_text(
|
||||
"🔄 <b>Обновление параметров доступа</b>\n\n"
|
||||
"Выберите новый тип доступа:",
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard,
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
# --- Очистка истёкших доступов ---
|
||||
|
||||
@router.callback_query(F.data == "admin_cleanup_expired")
|
||||
@check_admin
|
||||
async def admin_cleanup(callback: CallbackQuery):
|
||||
"""Очистка истёкших доступов."""
|
||||
deactivated = await db.deactivate_expired_users()
|
||||
await callback.answer(f"🧹 Деактивировано пользователей: {deactivated}", show_alert=True)
|
||||
await admin_panel(callback)
|
||||
Reference in New Issue
Block a user