Files
marzban_tg_bot/handlers/admin.py
2026-01-11 07:07:32 +03:00

850 lines
38 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>{code}</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 = "🏷 <b>Управление промокодами:</b>\n\n"
kb_buttons = []
now = datetime.now()
for p in promos:
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:
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
# Filter expired
if exp_dt and exp_dt < now:
continue
exp_str = exp_dt.strftime('%d.%m.%Y') if exp_dt else ""
is_unl = p['is_unlimited']
type_str = " (VIP)" if is_unl else f" (-{p['discount']}%)"
text += (
f"🔹 <code>{p['code']}</code>{type_str}\n"
f" Осталось: {p['uses_left']} | До: {exp_str}\n\n"
)
# Add delete button for each promo
kb_buttons.append([InlineKeyboardButton(text=f"❌ Удалить {p['code']}", callback_data=f"admin_promo_del_{p['code']}")])
kb_buttons.append([InlineKeyboardButton(text=" Создать промокод", callback_data="admin_create_promo")])
kb_buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_panel")])
await callback.message.edit_text(text, reply_markup=InlineKeyboardMarkup(inline_keyboard=kb_buttons), parse_mode="HTML")
@router.callback_query(F.data.startswith("admin_promo_del_"))
async def admin_delete_promo_bot(callback: CallbackQuery):
code = callback.data.replace("admin_promo_del_", "")
await db.delete_promo_code(code)
await callback.answer(f"✅ Промокод {code} удален")
await admin_promos(callback)
@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>{code}</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"✅ <b>Промокод создан!</b>\n\n"
f"Code: <code>{data['code']}</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"👤 <b>Пользователь:</b> <a href='tg://user?id={user['user_id']}'>{username_display}</a>\n"
f"🆔 ID: <code>{user['user_id']}</code>\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_set_{user_id}")],
[InlineKeyboardButton(text="✏️ Лимит ГБ", callback_data=f"adm_usr_gb_{user_id}"),
InlineKeyboardButton(text="🔄 Сброс", callback_data=f"adm_usr_reset_{user_id}")],
[InlineKeyboardButton(text="📋 План (Admin)", callback_data=f"adm_usr_plan_{user_id}"),
InlineKeyboardButton(text="✉️ Написать", callback_data=f"adm_usr_msg_{user_id}")],
[status_btn]
]
# Только если есть активная дата подписки
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="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("Ошибка. Введите целое число.")
# Set Fixed Expiry
@router.callback_query(F.data.startswith("adm_usr_set_"))
async def adm_usr_set_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"
"0 - Истечет сразу\n"
"36500 - Вечная подписка",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Отмена", callback_data=f"adm_sel_{user_id}")]])
)
await state.set_state(AdminUserStates.waiting_for_fixed_days)
@router.message(AdminUserStates.waiting_for_fixed_days)
async def adm_usr_set_process(message: Message, state: FSMContext):
try:
days = int(message.text)
data = await state.get_data()
user_id = data['target_user_id']
if days > 10000:
new_date = datetime(2099, 12, 31)
else:
new_date = datetime.now() + timedelta(days=days)
await db.execute("UPDATE users SET subscription_until = $1 WHERE user_id = $2", new_date, user_id)
# Sync to Marzban
user = await db.get_user(user_id)
marz_user = await marzban.get_user(user['marzban_username'])
# Marzban treats 0 or negative as no limit or infinity?
# Actually in our server.py we used:
days_left = (new_date - datetime.now()).days + 1 if days > 0 else 0
marz_days = days_left if days < 10000 else 0
await marzban.modify_user(
user['marzban_username'],
(user['data_limit'] / (1024**3)),
expire_timestamp=marz_days if days > 0 else 1 # 1 sec if expired
)
await message.answer(f"✅ Срок установлен: {days} дней")
await show_user_panel(message, user_id)
await state.clear()
except Exception as e:
await message.answer(f"❌ Ошибка: {e}")
# Set Plan (All plans visible to admin)
@router.callback_query(F.data.startswith("adm_usr_plan_"))
async def adm_usr_plan_list(callback: CallbackQuery):
user_id = int(callback.data.split("_")[3])
kb_btns = []
# Show ALL plans from CONFIG to admin
for pid, p in PLANS.items():
kb_btns.append([InlineKeyboardButton(text=f"{p['name']} ({p['days']}d / {p['data_limit'] or ''}GB)",
callback_data=f"adm_setplan_{user_id}_{pid}")])
kb_btns.append([InlineKeyboardButton(text="◀️ Отмена", callback_data=f"adm_sel_{user_id}")])
await callback.message.edit_text(
f"Выберите тарифный план для применения пользователю {user_id}:",
reply_markup=InlineKeyboardMarkup(inline_keyboard=kb_btns)
)
@router.callback_query(F.data.startswith("adm_setplan_"))
async def adm_usr_plan_process(callback: CallbackQuery):
parts = callback.data.split("_")
user_id = int(parts[2])
plan_id = parts[3]
plan = PLANS.get(plan_id)
if not plan:
await callback.answer("Ошибка: Тариф не найден", show_alert=True)
return
user = await db.get_user(user_id)
if not user: return
total_days = plan['days']
data_limit_gb = plan['data_limit']
limit_bytes = int(data_limit_gb * (1024**3)) if data_limit_gb > 0 else 999999 * (1024**3)
# Update DB
await db.execute("UPDATE users SET data_limit = $1 WHERE user_id = $2", limit_bytes, user_id)
new_date = datetime.now() + timedelta(days=total_days)
await db.execute("UPDATE users SET subscription_until = $1 WHERE user_id = $2", new_date, user_id)
# Sync to Marzban
marz_days = total_days if total_days > 0 else 0
marz_limit = data_limit_gb if data_limit_gb > 0 else 0
try:
await marzban.modify_user(user['marzban_username'],
marz_limit,
marz_days if marz_days > 0 else 1)
await callback.answer(f"✅ План {plan['name']} успешно применен!", show_alert=True)
except Exception as e:
await callback.answer(f"⚠️ БД обновлена, но Marzban вернул ошибку: {e}", show_alert=True)
await show_user_panel(callback, user_id)
# 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')
# 0 in our logic means unlimited
marz_limit = limit_gb if limit_gb > 0 else 0
await marzban.modify_user(user['marzban_username'], marz_limit, status=current_status, expire_timestamp=expire_ts)
# Store in DB
limit_bytes = int(limit_gb * (1024**3)) if limit_gb > 0 else 999999 * (1024**3)
await db.execute("UPDATE users SET data_limit = $1 WHERE user_id = $2", limit_bytes, user_id)
await message.answer(f"✅ Лимит изменен на {limit_gb if limit_gb > 0 else ''} 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"⚠️ <b>Вы уверены, что хотите удалить подписку у пользователя {user_id}?</b>\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)