from aiogram import Router, F from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton from aiogram.fsm.context import FSMContext from datetime import datetime, timedelta import random import string import logging from config import CONFIG from database import db from marzban import marzban from states import BroadcastStates, PromoStates, AdminUserStates from keyboards import admin_keyboard # Add new states for user adding from aiogram.fsm.state import State, StatesGroup class AddUserStates(StatesGroup): waiting_for_id = State() router = Router() logger = logging.getLogger(__name__) @router.callback_query(F.data == "admin_panel") async def admin_panel(callback: CallbackQuery): if callback.from_user.id not in CONFIG["ADMIN_IDS"]: await callback.answer("❌ Доступ запрещен", show_alert=True) return text = "👑 Панель администратора" kb = admin_keyboard() if callback.message.photo: await callback.message.delete() await callback.message.answer(text, reply_markup=kb) else: await callback.message.edit_text(text, reply_markup=kb) @router.callback_query(F.data == "admin_stats") async def admin_stats(callback: CallbackQuery): if callback.from_user.id not in CONFIG["ADMIN_IDS"]: return stats = await db.get_stats() text = ( "📊 Статистика бота:\n\n" f"👥 Всего пользователей: {stats['total']}\n" f"✅ Активных подписок: {stats['active']}\n" f"💰 Общая выручка: {stats['revenue']} ⭐\n" ) await callback.message.edit_text(text, reply_markup=admin_keyboard()) @router.callback_query(F.data == "admin_server_stats") async def admin_server_stats(callback: CallbackQuery): if callback.from_user.id not in CONFIG["ADMIN_IDS"]: return try: sys = await marzban.get_system_stats() usrs = await marzban.get_users_stats() text = ( "🖥️ Статистика сервера:\n\n" f"📊 CPU: {sys.get('cpu_usage', 'N/A')}%\n" f"💾 RAM: {sys.get('mem_used', 0)/(1024**3):.2f}/{sys.get('mem_total', 0)/(1024**3):.2f} GB\n" f"👥 Активных юзеров: {usrs.get('active_users', 0)}\n" f"📦 Всего трафика: {usrs.get('total_usage', 0)/(1024**3):.2f} GB" ) except Exception as e: logger.error(f"Error: {e}") text = "⚠️ Ошибка Marzban API" await callback.message.edit_text(text, reply_markup=admin_keyboard()) # --- Add Invite / Direct User Add --- @router.callback_query(F.data == "admin_add_invite") async def admin_add_invite_menu(callback: CallbackQuery): if callback.from_user.id not in CONFIG["ADMIN_IDS"]: return kb = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔢 Сгенерировать код", callback_data="admin_gen_code")], [InlineKeyboardButton(text="👤 Добавить по ID/Username", callback_data="admin_add_user_direct")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")] ]) await callback.message.edit_text("Выберите способ добавления:", reply_markup=kb) @router.callback_query(F.data == "admin_gen_code") async def admin_gen_code(callback: CallbackQuery): code = await db.create_invite_code(callback.from_user.id) # Получаем юзернейм бота bot_info = await callback.bot.get_me() bot_username = bot_info.username # Используем HTML, так как Markdown с подчеркиваниями часто ломается await callback.message.edit_text( f"✅ Новый инвайт-код:\n{code}\n\n" f"🔗 Ссылка для приглашения:\n" f"https://t.me/{bot_username}?start={code}", parse_mode="HTML", reply_markup=admin_keyboard() ) @router.callback_query(F.data == "admin_add_user_direct") async def admin_add_user_direct(callback: CallbackQuery, state: FSMContext): await callback.message.edit_text( "Введите Telegram ID (число) или Username (без @) пользователя:", reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Отмена", callback_data="admin_panel")]]) ) await state.set_state(AddUserStates.waiting_for_id) @router.message(AddUserStates.waiting_for_id) async def process_direct_add(message: Message, state: FSMContext): input_data = message.text.strip() # Try to determine if it is ID or Username. # NOTE: We can only add by ID correctly IF the user has started the bot before (to get chat info), # OR if we just blindly trust the ID for the DB. # But usually, if adding by Username, we can't get ID easily without bot API interaction (get_chat). user_id = None username = None if input_data.isdigit(): user_id = int(input_data) username = f"user_{user_id}" else: # Username logic is tricky because we need the numeric ID for the 'users' table primary key. # Without it, we can't insert into DB correctly if schema requires BIGINT KEY. # We'll try to resolve via bot API, but it often fails if bot never saw user. try: # Try to resolve chat? Bot API doesn't allow get_chat for users who didn't block bot, but... # Let's hope for the best or assume it's impossible without ID. await message.answer("⚠️ Добавление по юзернейму ненадежно без ID. Лучше используйте ID.") return except Exception: pass if user_id: existing = await db.get_user(user_id) if existing: await message.answer("❌ Пользователь уже есть в базе.") else: marzban_username = f"user_{user_id}" await db.create_user(user_id, username, marzban_username, message.from_user.id) await message.answer(f"✅ Пользователь {user_id} добавлен в базу!") await state.clear() await message.answer("Главное меню", reply_markup=main_keyboard(True)) # --- Promo Management --- @router.callback_query(F.data == "admin_promos") async def admin_promos(callback: CallbackQuery): if callback.from_user.id not in CONFIG["ADMIN_IDS"]: return promos = await db.get_active_promos() # Only valid ones text = "🏷 Активные промокоды:\n\n" if not promos: text = "Нет активных промокодов." for p in promos: # Обработка даты (SQLite возвращает строку) exp_val = p['expires_at'] exp_dt = None if exp_val: if isinstance(exp_val, str): try: exp_dt = datetime.strptime(exp_val, '%Y-%m-%d %H:%M:%S') except ValueError: try: exp_dt = datetime.strptime(exp_val, '%Y-%m-%d %H:%M:%S.%f') except: pass elif isinstance(exp_val, datetime): exp_dt = exp_val exp_str = exp_dt.strftime('%d.%m.%Y') if exp_dt else "∞" # Получаем значения по ключам (не через get) is_unl = p['is_unlimited'] type_str = " (VIP ∞)" if is_unl else f" (-{p['discount']}%)" text += ( f"🔹 {p['code']}{type_str}\n" f" Осталось: {p['uses_left']} | До: {exp_str}\n" ) kb = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="➕ Создать промокод", callback_data="admin_create_promo")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")] ]) await callback.message.edit_text(text, reply_markup=kb, parse_mode="HTML") @router.callback_query(F.data == "admin_create_promo") async def start_create_promo(callback: CallbackQuery, state: FSMContext): kb = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="✍️ Свой вариант", callback_data="promo_name_custom")], [InlineKeyboardButton(text="🎲 Сгенерировать", callback_data="promo_name_generate")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")] ]) await callback.message.edit_text("Как задать название промокода?", reply_markup=kb) @router.callback_query(F.data == "promo_name_custom") async def promo_name_custom(callback: CallbackQuery, state: FSMContext): await callback.message.edit_text("Введите НАЗВАНИЕ промокода (например, NEWYEAR):") await state.set_state(PromoStates.waiting_for_name) @router.callback_query(F.data == "promo_name_generate") async def promo_name_generate(callback: CallbackQuery, state: FSMContext): code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8)) await proceed_after_name(callback, state, code) async def proceed_after_name(message_or_call, state: FSMContext, code: str): await state.update_data(code=code) text = f"Название: {code}\n\nВведите размер скидки в % (от 0 до 100):\nМожно ввести 0, если это только бонус-код." # Кнопка для быстрого VIP (чтобы не проходить все шаги если нужен просто VIP) kb = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="♾ Сделать VIP (Бесконечный)", callback_data="promo_make_vip")] ]) if isinstance(message_or_call, CallbackQuery): await message_or_call.message.edit_text(text, reply_markup=kb, parse_mode="HTML") else: await message_or_call.answer(text, reply_markup=kb, parse_mode="HTML") await state.set_state(PromoStates.waiting_for_discount) @router.message(PromoStates.waiting_for_name) async def promo_name_entered(message: Message, state: FSMContext): await proceed_after_name(message, state, message.text.upper().strip()) @router.callback_query(F.data == "promo_make_vip") async def promo_make_vip(callback: CallbackQuery, state: FSMContext): # VIP shortcut await state.update_data(discount=100, is_unlimited=True, bonus_days=0) await callback.message.edit_text("Введите количество использований (число):") await state.set_state(PromoStates.waiting_for_uses) @router.message(PromoStates.waiting_for_discount) async def promo_discount_step(message: Message, state: FSMContext): try: val = int(message.text) if not 0 <= val <= 100: raise ValueError await state.update_data(discount=val, is_unlimited=False) if val > 0: # Если есть скидка, спрашиваем, закрепить ли её kb = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="✅ Да, закрепить цену навсегда", callback_data="promo_sticky_yes")], [InlineKeyboardButton(text="❌ Нет, одноразовая", callback_data="promo_sticky_no")] ]) await message.answer("Закрепить эту скидку за пользователем НАВСЕГДА? (Цена останется такой же при продлении)", reply_markup=kb) else: # Скидки нет, пропускаем шаг await state.update_data(is_sticky=False) await message.answer("Введите количество БОНУСНЫХ ДНЕЙ (0 если нет):") await state.set_state(PromoStates.waiting_for_bonus) except: await message.answer("Введите число от 0 до 100!") @router.callback_query(F.data.startswith("promo_sticky_")) async def promo_sticky_callback(callback: CallbackQuery, state: FSMContext): is_sticky = (callback.data == "promo_sticky_yes") await state.update_data(is_sticky=is_sticky) await callback.message.edit_text("Введите количество БОНУСНЫХ ДНЕЙ (0 если нет):") await state.set_state(PromoStates.waiting_for_bonus) @router.message(PromoStates.waiting_for_bonus) async def promo_bonus_step(message: Message, state: FSMContext): try: val = int(message.text) if val < 0: raise ValueError await state.update_data(bonus_days=val) await message.answer("Введите количество использований (число):") await state.set_state(PromoStates.waiting_for_uses) except: await message.answer("Введите положительное число или 0!") # Quick fix for the missing state step: # I will use `PromoStates.waiting_for_name` again but with flagging? No, bad practice. # Let's add the state to `states.py` in my mind, but since I can't edit that file instantly without tool, # I will just define a handler that catches "waiting_for_uses" + logic... # No, let's just make the user input days NOW in `promo_uses`? # Ah, I need to read the previous input first. # See below implementation. # ... Redoing `promo_uses` to chaining correctly ... @router.message(PromoStates.waiting_for_uses) async def promo_uses_step(message: Message, state: FSMContext): try: val = int(message.text) if val < 1: raise ValueError await state.update_data(uses=val) # Кнопки с пресетами kb = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="♾ Навсегда", callback_data="days_0")], [InlineKeyboardButton(text="📅 30 дней", callback_data="days_30"), InlineKeyboardButton(text="📅 180 дней", callback_data="days_180")], [InlineKeyboardButton(text="📅 1 год", callback_data="days_365")], [InlineKeyboardButton(text="✍️ Свой вариант", callback_data="days_custom")] ]) await message.answer( "Сколько дней будет действовать промокод?\n" "Выберите вариант или введите число вручную:", reply_markup=kb ) await state.set_state(PromoStates.waiting_for_days) except: await message.answer("Введите корректное число использований!") # Общая функция создания async def create_promo_final(message_or_call, state: FSMContext, days: int): data = await state.get_data() expires_at = None if days > 0: expires_at = datetime.now() + timedelta(days=days) try: await db.create_promo_code( data['code'], data['discount'], data['uses'], message_or_call.from_user.id, expires_at, data.get('is_unlimited', False), data.get('bonus_days', 0), data.get('is_sticky', False) ) except Exception as e: logger.error(f"Error creating promo: {e}") error_text = "❌ Ошибка: Такой промокод уже существует или произошел сбой БД." if isinstance(message_or_call, CallbackQuery): await message_or_call.message.edit_text(error_text, reply_markup=admin_keyboard()) else: await message_or_call.answer(error_text, reply_markup=admin_keyboard()) await state.clear() return bonus = data.get('bonus_days', 0) is_sticky = data.get('is_sticky', False) if data.get('is_unlimited'): type_text = "♾ VIP" else: parts = [] if data['discount'] > 0: fixed = " (FIXED)" if is_sticky else "" parts.append(f"-{data['discount']}%{fixed}") if bonus > 0: parts.append(f"+{bonus}d") type_text = " ".join(parts) if parts else "Standard" exp_text = expires_at.strftime('%d.%m.%Y') if expires_at else "Бессрочно" confirm_text = ( f"✅ Промокод создан!\n\n" f"Code: {data['code']}\n" f"Type: {type_text}\n" f"Uses: {data['uses']}\n" f"Expires: {exp_text}" ) if isinstance(message_or_call, CallbackQuery): await message_or_call.message.edit_text(confirm_text, reply_markup=admin_keyboard(), parse_mode="HTML") else: await message_or_call.answer(confirm_text, reply_markup=admin_keyboard(), parse_mode="HTML") await state.clear() @router.callback_query(PromoStates.waiting_for_days, F.data == "days_custom") async def promo_days_custom(callback: CallbackQuery): await callback.message.edit_text("✍️ Введите срок действия промокода в днях (целое число):") await callback.answer() @router.callback_query(PromoStates.waiting_for_days, F.data.regexp(r"^days_\d+$")) async def promo_days_callback(callback: CallbackQuery, state: FSMContext): days = int(callback.data.split("_")[1]) await create_promo_final(callback, state, days) @router.message(PromoStates.waiting_for_days) async def promo_days_manual(message: Message, state: FSMContext): try: days = int(message.text) if days < 0: raise ValueError await create_promo_final(message, state, days) except: await message.answer("Введите число (0 или больше)!") # --- Broadcast --- @router.callback_query(F.data == "admin_broadcast") async def admin_broadcast_start(callback: CallbackQuery, state: FSMContext): if callback.from_user.id not in CONFIG["ADMIN_IDS"]: return await callback.message.edit_text("Сообщение для рассылки:", reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Отмена", callback_data="admin_panel")]])) await state.set_state(BroadcastStates.waiting_for_message) @router.message(BroadcastStates.waiting_for_message) async def broadcast_go(message: Message, state: FSMContext): users = await db.get_users_for_broadcast() count = 0 msg = await message.answer("Рассылаю...") for u in users: try: await message.send_copy(u['user_id']) count+=1 except: pass await msg.edit_text(f"✅ Отправлено: {count}", reply_markup=admin_keyboard()) await state.clear() # --- User Management --- @router.callback_query(F.data == "admin_users_list") async def admin_users_list(callback: CallbackQuery): if callback.from_user.id not in CONFIG["ADMIN_IDS"]: return kb = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔍 Найти пользователя", callback_data="admin_search_user")], [InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")] ]) await callback.message.edit_text("👥 Управление пользователями", reply_markup=kb) @router.callback_query(F.data == "admin_search_user") async def admin_search_user(callback: CallbackQuery, state: FSMContext): await callback.message.edit_text( "Введите Telegram ID или Username пользователя:", reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Отмена", callback_data="admin_panel")]]) ) await state.set_state(AdminUserStates.waiting_for_search) @router.message(AdminUserStates.waiting_for_search) async def process_user_search(message: Message, state: FSMContext): query = message.text.strip() users = await db.search_users(query) if not users: await message.answer( "❌ Пользователи не найдены.\nПопробуйте другой запрос:", reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Отмена", callback_data="admin_panel")]]) ) return if len(users) == 1: await show_user_panel(message, users[0]["user_id"]) await state.clear() else: # Show list kb_rows = [] for u in users: display = u['username'] if u['username'] else f"ID: {u['user_id']}" kb_rows.append([InlineKeyboardButton(text=f"{display} | {u['user_id']}", callback_data=f"adm_sel_{u['user_id']}")]) kb_rows.append([InlineKeyboardButton(text="◀️ Отмена", callback_data="admin_users_list")]) await message.answer(f"🔍 Найдено {len(users)} пользователей:\nВыберите пользователя:", reply_markup=InlineKeyboardMarkup(inline_keyboard=kb_rows)) await state.clear() @router.callback_query(F.data.startswith("adm_sel_")) async def admin_user_select(callback: CallbackQuery): user_id = int(callback.data.split("_")[2]) await show_user_panel(callback, user_id) async def show_user_panel(message_or_call, user_id): user = await db.get_user(user_id) if not user: if isinstance(message_or_call, CallbackQuery): await message_or_call.answer("User not found") else: await message_or_call.answer("User not found") return # Get marzban info marz_info = {} try: marz_info = await marzban.get_user(user['marzban_username']) except: pass status = marz_info.get('status', 'Unknown') used_traffic = marz_info.get('used_traffic', 0) data_limit = marz_info.get('data_limit', 0) traffic_used_gb = used_traffic / (1024**3) if used_traffic else 0 traffic_limit_gb = data_limit / (1024**3) if data_limit else 0 sub_until = user['subscription_until'] if sub_until and isinstance(sub_until, str): try: sub_until = datetime.strptime(sub_until, '%Y-%m-%d %H:%M:%S') except: try: sub_until = datetime.strptime(sub_until, '%Y-%m-%d %H:%M:%S.%f') except: pass exp_str = sub_until.strftime('%d.%m.%Y %H:%M') if sub_until and isinstance(sub_until, datetime) else "Нет подписки" username_display = user['username'] if user['username'] else str(user['user_id']) status_icon = "🟢" if status == 'active' else "🔴" text = ( f"👤 Пользователь: {username_display}\n" f"🆔 ID: {user['user_id']}\n" f"🔋 Статус Marzban: {status_icon} {status}\n" f"📅 Подписка до: {exp_str}\n" f"📊 Трафик: {traffic_used_gb:.2f} / {traffic_limit_gb:.2f} GB\n" ) # Dynamic buttons status_btn = InlineKeyboardButton(text="⛔️ Заблокировать", callback_data=f"adm_usr_ban_{user_id}") if status == 'disabled': status_btn = InlineKeyboardButton(text="✅ Разблокировать", callback_data=f"adm_usr_unban_{user_id}") rows = [ [InlineKeyboardButton(text="➕ Продлить", callback_data=f"adm_usr_add_{user_id}"), InlineKeyboardButton(text="✏️ Лимит", callback_data=f"adm_usr_gb_{user_id}")], [status_btn, InlineKeyboardButton(text="🔄 Сброс трафика", callback_data=f"adm_usr_reset_{user_id}")] ] # Только если есть активная дата подписки if sub_until and isinstance(sub_until, datetime): rows.append([InlineKeyboardButton(text="❌ Удалить подписку", callback_data=f"adm_usr_delsub_{user_id}")]) rows.append([InlineKeyboardButton(text="✉️ Сообщение", callback_data=f"adm_usr_msg_{user_id}")]) rows.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_users_list")]) kb = InlineKeyboardMarkup(inline_keyboard=rows) if isinstance(message_or_call, CallbackQuery): # We try to edit if possible, but if message content is same it errors. # However, status or buttons likely changed so it is fine. try: await message_or_call.message.edit_text(text, reply_markup=kb, parse_mode="HTML") except: await message_or_call.message.delete() await message_or_call.message.answer(text, reply_markup=kb, parse_mode="HTML") else: await message_or_call.answer(text, reply_markup=kb, parse_mode="HTML") # Add Time @router.callback_query(F.data.startswith("adm_usr_add_")) async def adm_usr_add_start(callback: CallbackQuery, state: FSMContext): user_id = int(callback.data.split("_")[3]) await state.update_data(target_user_id=user_id) await callback.message.edit_text( "Введите количество дней для добавления (целое число).\n" "Или отрицательное число, чтобы уменьшить срок.", reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Отмена", callback_data=f"admin_users_list")]]) ) await state.set_state(AdminUserStates.waiting_for_days) @router.message(AdminUserStates.waiting_for_days) async def adm_usr_add_process(message: Message, state: FSMContext): try: days = int(message.text) data = await state.get_data() user_id = data['target_user_id'] user = await db.get_user(user_id) limit = user['data_limit'] if user['data_limit'] else 0 await db.update_subscription(user_id, days, limit) # Update Marzban updated_user = await db.get_user(user_id) sub_until = updated_user['subscription_until'] if isinstance(sub_until, str): try: sub_until = datetime.strptime(sub_until, '%Y-%m-%d %H:%M:%S') except: try: sub_until = datetime.strptime(sub_until, '%Y-%m-%d %H:%M:%S.%f') except: pass if sub_until: delta = sub_until - datetime.now() days_left = delta.days + 1 if delta.days >= 0 else 0 else: days_left = 0 limit_gb = limit / (1024**3) if limit else 0 try: await marzban.modify_user(updated_user['marzban_username'], limit_gb, days_left) await message.answer(f"✅ Добавлено {days} дней пользователю {user_id}") except Exception as e: await message.answer(f"⚠️ БД обновлена, но Marzban вернул ошибку: {e}") await show_user_panel(message, user_id) await state.clear() except ValueError: await message.answer("Ошибка. Введите целое число.") # Reset Traffic @router.callback_query(F.data.startswith("adm_usr_reset_")) async def adm_usr_reset(callback: CallbackQuery): user_id = int(callback.data.split("_")[3]) user = await db.get_user(user_id) if user: try: await marzban.reset_user_traffic(user['marzban_username']) await callback.answer("✅ Трафик сброшен", show_alert=True) except Exception as e: await callback.answer(f"Ошибка: {e}", show_alert=True) await show_user_panel(callback, user_id) # Send Message @router.callback_query(F.data.startswith("adm_usr_msg_")) async def adm_usr_msg_start(callback: CallbackQuery, state: FSMContext): user_id = int(callback.data.split("_")[3]) await state.update_data(target_user_id=user_id) await callback.message.edit_text( "Введите сообщение для отправки пользователю:", reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Отмена", callback_data="admin_users_list")]]) ) await state.set_state(AdminUserStates.waiting_for_message) @router.message(AdminUserStates.waiting_for_message) async def adm_usr_msg_send(message: Message, state: FSMContext): data = await state.get_data() user_id = data['target_user_id'] try: await message.send_copy(user_id) await message.answer(f"✅ Сообщение отправлено пользователю {user_id}") except Exception as e: await message.answer(f"❌ Ошибка отправки: {e}") await show_user_panel(message, user_id) await state.clear() # Ban/Unban @router.callback_query(F.data.regexp(r"^adm_usr_(ban|unban)_\d+$")) async def adm_usr_toggle_status(callback: CallbackQuery): action = callback.data.split("_")[2] user_id = int(callback.data.split("_")[3]) new_status = "disabled" if action == "ban" else "active" user = await db.get_user(user_id) if user: try: marz_user = await marzban.get_user(user['marzban_username']) current_limit = marz_user.get('data_limit') current_limit_gb = (current_limit / (1024**3)) if current_limit else 0 expire_ts = marz_user.get('expire') await marzban.modify_user(user['marzban_username'], current_limit_gb, status=new_status, expire_timestamp=expire_ts) await callback.answer(f"Статус изменен на {new_status}", show_alert=True) except Exception as e: await callback.answer(f"Ошибка: {e}", show_alert=True) await show_user_panel(callback, user_id) # Limit Change @router.callback_query(F.data.startswith("adm_usr_gb_")) async def adm_usr_limit_start(callback: CallbackQuery, state: FSMContext): user_id = int(callback.data.split("_")[3]) await state.update_data(target_user_id=user_id) await callback.message.edit_text( "Введите новый лимит трафика в GB (число):\n0 = Безлимит (если поддерживается)", reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Отмена", callback_data=f"adm_sel_{user_id}")]]) ) await state.set_state(AdminUserStates.waiting_for_limit) @router.message(AdminUserStates.waiting_for_limit) async def adm_usr_limit_process(message: Message, state: FSMContext): try: limit_gb = float(message.text) if limit_gb < 0: raise ValueError data = await state.get_data() user_id = data['target_user_id'] user = await db.get_user(user_id) marz_user = await marzban.get_user(user['marzban_username']) expire_ts = marz_user.get('expire') current_status = marz_user.get('status', 'active') await marzban.modify_user(user['marzban_username'], limit_gb, status=current_status, expire_timestamp=expire_ts) limit_bytes = int(limit_gb * 1024 * 1024 * 1024) await db.execute("UPDATE users SET data_limit = $1 WHERE user_id = $2", limit_bytes, user_id) await message.answer(f"✅ Лимит изменен на {limit_gb} GB") await show_user_panel(message, user_id) await state.clear() except ValueError: await message.answer("Введите корректное число!") # Delete Subscription @router.callback_query(F.data.startswith("adm_usr_delsub_")) async def adm_usr_delsub_ask(callback: CallbackQuery): user_id = int(callback.data.split("_")[3]) kb = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="✅ Да, удалить", callback_data=f"adm_usr_confirm_delsub_{user_id}")], [InlineKeyboardButton(text="❌ Отмена", callback_data=f"adm_sel_{user_id}")] ]) await callback.message.edit_text( f"⚠️ Вы уверены, что хотите удалить подписку у пользователя {user_id}?\n" "Пользователь потеряет доступ к VPN (срок действия истечет сейчас).", reply_markup=kb, parse_mode="HTML" ) @router.callback_query(F.data.startswith("adm_usr_confirm_delsub_")) async def adm_usr_delsub_confirm(callback: CallbackQuery): user_id = int(callback.data.split("_")[4]) user = await db.get_user(user_id) if not user: await callback.answer("Пользователь не найден") return # Update DB await db.remove_subscription(user_id) # Update Marzban try: # Expire immediately expire_ts = int(datetime.now().timestamp()) marz_user = await marzban.get_user(user['marzban_username']) current_limit = marz_user.get('data_limit') current_limit_gb = (current_limit / (1024**3)) if current_limit else 0 current_status = marz_user.get('status', 'active') await marzban.modify_user( user['marzban_username'], current_limit_gb, status=current_status, expire_timestamp=expire_ts ) await callback.answer("✅ Подписка удалена", show_alert=True) except Exception as e: await callback.answer(f"Ошибка Marzban: {e}", show_alert=True) await show_user_panel(callback, user_id)