Улучшение взаимодействия и добавление веб-приложения

This commit is contained in:
2026-01-09 01:20:30 +03:00
parent eed252d52e
commit 2472947c1f
16 changed files with 2972 additions and 969 deletions

7
handlers/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
from . import user, admin, payment
routers = [
user.router,
admin.router,
payment.router
]

735
handlers/admin.py Normal file
View File

@@ -0,0 +1,735 @@
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"
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"🔹 <code>{p['code']}</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>{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_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"⚠️ <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)

217
handlers/payment.py Normal file
View File

@@ -0,0 +1,217 @@
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, LabeledPrice, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.fsm.context import FSMContext
from datetime import datetime
import logging
from config import CONFIG, PLANS
from database import db
from marzban import marzban
from keyboards import main_keyboard
router = Router()
logger = logging.getLogger(__name__)
async def grant_subscription(user_id: int, plan_id: str, promo_code: str, amount: int, bonus_days: int = 0):
total_days = 0
data_limit = 0
plan_name = "VIP Sub"
marzban_days = 0
if plan_id:
plan = PLANS[plan_id]
total_days = plan['days'] + bonus_days
marzban_days = total_days
data_limit = plan['data_limit']
plan_name = plan['name']
else:
# VIP case without plan
total_days = 365 * 99 # For DB (99 years)
marzban_days = 0 # For Marzban (Unlimited/None)
data_limit = 0 # Unlimited
plan_name = "VIP"
user = await db.get_user(user_id)
tg_username = user['username']
note = f"@{tg_username}" if tg_username else ""
sub_until = user['subscription_until']
if isinstance(sub_until, str):
try:
sub_until = datetime.strptime(sub_until, '%Y-%m-%d %H:%M:%S')
except ValueError:
try:
sub_until = datetime.strptime(sub_until, '%Y-%m-%d %H:%M:%S.%f')
except: pass
# Marzban
try:
marzban_username = user['marzban_username']
resp = None
# Если есть подписка и она активна (и не бесконечна, хотя тут не важно)
is_sub_active = sub_until and sub_until > datetime.now()
if is_sub_active:
logger.info(f"Attempting to modify existing user: {marzban_username}")
resp = await marzban.modify_user(
marzban_username,
data_limit,
marzban_days,
note
)
if isinstance(resp, dict) and resp.get('detail') == 'User not found':
logger.info(f"User {marzban_username} missing in Marzban, re-creating...")
resp = await marzban.create_user(
marzban_username,
data_limit,
marzban_days,
note
)
else:
logger.info(f"Creating/Reactivating user: {marzban_username}")
resp = await marzban.create_user(
marzban_username,
data_limit,
marzban_days,
note
)
except Exception as e:
logger.error(f"Marzban error in grant_subscription: {e}")
# DB
await db.update_subscription(user_id, total_days, data_limit)
await db.add_payment(
user_id,
plan_id or "vip", # Store 'vip' as plan name/id
amount,
promo_code
)
if promo_code:
await db.decrement_promo_usage(promo_code)
# Return dummy plan dict for display
return {'name': plan_name, 'days': total_days, 'data_limit': data_limit}, total_days
@router.callback_query(F.data == "pay_now")
async def process_payment(callback: CallbackQuery, state: FSMContext):
data = await state.get_data()
plan_id = data.get('selected_plan')
is_vip = data.get('is_unlimited_promo')
if not plan_id and not is_vip:
await callback.answer("❌ Сессия истекла. Пожалуйста, выберите тариф снова.", show_alert=True)
return
# Если VIP без плана, ставим дефолтные значения
plan_name = "VIP"
plan_days = 3650
plan_limit = 0
base_price = 0
if plan_id:
plan = PLANS[plan_id]
plan_name = plan['name']
plan_days = plan['days']
plan_limit = plan['data_limit']
base_price = plan['price']
final_price = int(data.get('final_price', base_price))
promo_code = data.get('promo_code')
bonus_days = data.get('bonus_days', 0)
if final_price <= 0:
sticky_msg = ""
if promo_code:
p_data = await db.get_promo_code(promo_code)
# Use explicit key access with fallback logic if needed, but keys exist
if p_data and p_data['is_sticky']:
user = await db.get_user(callback.from_user.id)
u_disc = user['personal_discount'] if user and user['personal_discount'] else 0
if p_data['discount'] > u_disc:
await db.set_user_discount(callback.from_user.id, p_data['discount'])
sticky_msg = "\n🔐 Скидка закреплена навсегда!"
plan, date_days = await grant_subscription(callback.from_user.id, plan_id, promo_code, 0, bonus_days)
await callback.message.edit_text(
f"✅ Подписка активирована бесплатно!\n\n"
f"План: {plan['name']}\n"
f"Срок: {date_days} дней\n"
f"Трафик: {plan['data_limit'] if plan['data_limit'] > 0 else ''} ГБ\n"
f"{sticky_msg}\n"
f"Настройте подключение в меню: 📊 Моя подписка",
reply_markup=main_keyboard(callback.from_user.id in CONFIG["ADMIN_IDS"])
)
await state.clear()
else:
# Создаем инвойс для Telegram Stars
await callback.message.answer_invoice(
title=f"Подписка VPN - {plan['name']}",
description=f"Трафик: {plan['data_limit']} ГБ на {plan['days']} дней",
payload=f"{plan_id}:{data.get('promo_code', '')}",
currency="XTR", # Telegram Stars
prices=[LabeledPrice(label=plan['name'], amount=final_price)],
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💳 Оплатить", pay=True)]
])
)
@router.pre_checkout_query()
async def checkout_process(pre_checkout_query):
await pre_checkout_query.answer(ok=True)
@router.message(F.successful_payment)
async def successful_payment(message: Message):
payment = message.successful_payment
plan_id, promo_code = payment.invoice_payload.split(":")
# We can reuse grant_subscription helper
promo_code = promo_code if promo_code else None
bonus_days = 0
sticky_text = ""
if promo_code:
# Fetch actual promo details
promo_data = await db.get_promo_code(promo_code)
if promo_data:
bonus_days = promo_data['bonus_days']
# STICKY LOGIC
# Access by key (sqlite3.Row has no .get method)
is_sticky = False
try:
is_sticky = promo_data['is_sticky']
except IndexError:
pass # Column missing?
if is_sticky:
user = await db.get_user(message.from_user.id)
current_discount = user['personal_discount'] if user and user['personal_discount'] else 0
new_discount = promo_data['discount']
if new_discount > current_discount:
await db.set_user_discount(message.from_user.id, new_discount)
sticky_text = f"\n🔐 <b>Скидка {new_discount}% закреплена за вами НАВСЕГДА!</b>"
plan, date_days = await grant_subscription(
message.from_user.id,
plan_id,
promo_code,
payment.total_amount,
bonus_days
)
await message.answer(
f"✅ Оплата успешна!\n\n"
f"Ваша подписка активирована на {date_days} дней.\n"
f"Трафик: {plan['data_limit']} ГБ\n"
f"{sticky_text}\n"
f"Получите конфигурацию через меню: 📊 Моя подписка",
reply_markup=main_keyboard(message.from_user.id in CONFIG["ADMIN_IDS"]),
parse_mode="HTML"
)

477
handlers/user.py Normal file
View File

@@ -0,0 +1,477 @@
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton, BufferedInputFile
from aiogram.fsm.context import FSMContext
from aiogram.filters import Command, StateFilter, CommandStart, CommandObject
from datetime import datetime
import qrcode
from qrcode import QRCode
import io
import logging
from config import CONFIG, PLANS
from database import db
from marzban import marzban
from states import InviteStates, PromoStates
from keyboards import main_keyboard, plans_keyboard
router = Router()
logger = logging.getLogger(__name__)
# Helper to check if sub is active
def is_active(user):
if not user:
return False
sub_until = user['subscription_until']
if not sub_until:
return False
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:
return False # Parse error
return sub_until > datetime.now()
@router.message(CommandStart(deep_link=True))
async def cmd_start_deep_link(message: Message, command: CommandObject, state: FSMContext):
# Если запуск по ссылке (инвайт код)
code = command.args
invite = await db.check_invite_code(code)
if invite:
user_id = message.from_user.id
# Проверяем, не зарегистрирован ли уже
existing_user = await db.get_user(user_id)
if existing_user:
await message.answer("Вы уже зарегистрированы! Инвайт-код не нужен.")
await show_main_menu(message, user_id)
return
username = message.from_user.username or f"user_{user_id}"
marzban_username = message.from_user.username.lower() if message.from_user.username else f"user_{user_id}"
await db.create_user(user_id, username, marzban_username, invite['created_by'])
await db.use_invite_code(code, user_id)
await message.answer("✅ Инвайт-код принят! Добро пожаловать.")
await show_main_menu(message, user_id)
else:
await message.answer("❌ Неверный или использованный инвайт-код.")
# Fallback to normal start logic check
await cmd_start(message, state)
@router.message(Command("start"))
async def cmd_start(message: Message, state: FSMContext):
user_id = message.from_user.id
user = await db.get_user(user_id)
# Авторегистрация админа
if user_id in CONFIG["ADMIN_IDS"] and not user:
username = message.from_user.username or f"user_{user_id}"
marzban_username = message.from_user.username.lower() if message.from_user.username else f"user_{user_id}"
await db.create_user(user_id, username, marzban_username)
user = await db.get_user(user_id)
if user:
await show_main_menu(message, user_id)
else:
await message.answer(
"👋 Добро пожаловать!\n\n"
"Для использования бота необходим инвайт-код.\n"
"Если у вас есть ссылка-приглашение, перейдите по ней.\n"
"Или введите ваш инвайт-код вручную:"
)
await state.set_state(InviteStates.waiting_for_code)
@router.message(Command("myid"), StateFilter("*"))
async def cmd_myid(message: Message):
username = f"@{message.from_user.username}" if message.from_user.username else "No username"
await message.answer(
f"👤 Ваш профиль:\n"
f"ID: `{message.from_user.id}`\n"
f"User: {username}",
parse_mode="Markdown"
)
async def show_main_menu(message: Message, user_id: int):
user = await db.get_user(user_id)
is_admin = user_id in CONFIG["ADMIN_IDS"]
active = is_active(user)
await message.answer(
f"Привет, {message.from_user.first_name}! 👋\n\n"
"Главное меню:",
reply_markup=main_keyboard(is_admin, active)
)
@router.message(InviteStates.waiting_for_code)
async def process_invite_code(message: Message, state: FSMContext):
code = message.text.strip().upper()
invite = await db.check_invite_code(code)
if invite:
user_id = message.from_user.id
username = message.from_user.username or f"user_{user_id}"
marzban_username = message.from_user.username.lower() if message.from_user.username else f"user_{user_id}"
await db.create_user(user_id, username, marzban_username, invite['created_by'])
await db.use_invite_code(code, user_id)
await message.answer("✅ Инвайт-код принят! Добро пожаловать.")
await show_main_menu(message, user_id)
await state.clear()
else:
await message.answer("❌ Неверный или использованный инвайт-код. Попробуйте еще раз:")
@router.callback_query(F.data == "my_subscription")
async def show_subscription(callback: CallbackQuery):
user = await db.get_user(callback.from_user.id)
if not user:
await callback.answer("❌ Пользователь не найден", show_alert=True)
return
sub_until = user['subscription_until']
if not is_active(user):
await callback.answer("❌ Подписка не активна. Купите подписку в главном меню.", show_alert=True)
return
# Handle datetime conversion
if isinstance(sub_until, str):
try:
sub_until = datetime.strptime(sub_until, '%Y-%m-%d %H:%M:%S')
except ValueError:
try:
sub_until = datetime.strptime(sub_until, '%Y-%m-%d %H:%M:%S.%f')
except:
pass
try:
marzban_user = await marzban.get_user(user['marzban_username'])
if isinstance(marzban_user, dict) and marzban_user.get('detail') == 'User not found':
logger.warning(f"User {user['marzban_username']} not found in Marzban, restoring...")
note = f"@{user['username']}" if user.get('username') else ""
# Restore with 30 days default or fetch from DB? Using 30 as per previous code
await marzban.create_user(user['marzban_username'], user['data_limit'] or 50, 30, note)
marzban_user = await marzban.get_user(user['marzban_username'])
used_traffic = marzban_user.get('used_traffic', 0) / (1024**3)
sub_url = marzban_user.get('subscription_url', 'Генерируется...')
if sub_url and sub_url.startswith('/'):
base = CONFIG.get('BASE_URL') or CONFIG['MARZBAN_URL']
sub_url = f"{base.rstrip('/')}{sub_url}"
# Check if unlimited (far future date)
if sub_until.year > 2090:
date_str = "♾ Бессрочно"
else:
date_str = sub_until.strftime('%d.%m.%Y %H:%M')
data_limit_gb = user['data_limit']
if data_limit_gb > 10000: # Assuming huge number is unlimited in our DB logic
limit_str = "♾ Безлимит"
else:
limit_str = f"{data_limit_gb} ГБ"
info_text = (
f"📊 Ваша подписка:\n\n"
f"⏰ Действует до: {date_str}\n"
f"📦 Лимит трафика: {limit_str}\n"
f"📊 Использовано: {used_traffic:.2f} ГБ\n\n"
f"🎫 Ссылка на подписку:\n"
f"`{sub_url}`"
)
except Exception as e:
logger.error(f"Error getting user info: {e}")
info_text = "⚠️ Ошибка получения данных. Попробуйте позже."
sub_url = "error"
# Клавиатура с кнопкой продления
sub_kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Продлить подписку", callback_data="buy_subscription")],
[InlineKeyboardButton(text="◀️ В главное меню", callback_data="back_to_main")]
])
try:
qr = QRCode(version=1, box_size=10, border=5)
qr.add_data(sub_url)
qr.make(fit=True)
qr_img = qr.make_image(fill_color="black", back_color="white")
img_buffer = io.BytesIO()
qr_img.save(img_buffer)
img_buffer.seek(0)
qr_file = BufferedInputFile(img_buffer.getvalue(), filename="subscription_qr.png")
await callback.message.delete()
await callback.message.answer_photo(
photo=qr_file,
caption=info_text,
reply_markup=sub_kb,
parse_mode="Markdown"
)
except Exception as e:
logger.error(f"Error sending subscription photo: {e}")
await callback.message.answer(info_text, reply_markup=sub_kb, parse_mode="Markdown")
async def calculate_final_price(base_price: int, discount: int) -> int:
return int(base_price * (100 - discount) / 100)
@router.callback_query(F.data == "buy_subscription")
async def show_plans(callback: CallbackQuery):
user = await db.get_user(callback.from_user.id)
personal_desc = user['personal_discount'] if user and user['personal_discount'] else 0
text = (
"💎 <b>Выберите тарифный план:</b>\n\n"
"Все планы включают:\n"
"• Высокую скорость\n"
"• Поддержку всех устройств"
)
if personal_desc > 0:
text += f"\n\n🔥 Ваша персональная скидка: <b>{personal_desc}%</b>"
kb = plans_keyboard()
if callback.message.photo:
await callback.message.delete()
await callback.message.answer(text, reply_markup=kb, parse_mode="HTML")
else:
await callback.message.edit_text(text, reply_markup=kb, parse_mode="HTML")
@router.callback_query(F.data.startswith("plan_"))
async def process_plan_selection(callback: CallbackQuery, state: FSMContext):
plan_id = callback.data.replace("plan_", "", 1)
plan = PLANS[plan_id]
await state.update_data(selected_plan=plan_id)
data = await state.get_data()
# Check User Personal Discount
user = await db.get_user(callback.from_user.id)
personal_dist = user['personal_discount'] if user and user['personal_discount'] else 0
# Check Promo Discount
promo_dist = data.get('discount', 0)
# Effective Discount = Max of personal or promo
effective_discount = max(personal_dist, promo_dist)
# Calculate Price
final_price = await calculate_final_price(plan['price'], effective_discount)
await state.update_data(final_price=final_price)
msg = (
f"💎 Тариф: <b>{plan['name']}</b>\n"
f"📅 Срок: {plan['days']} дней\n"
f"📦 Трафик: {plan.get('limit_gb', '')} ГБ\n"
f"━━━━━━━━━━━━━━━\n"
)
if effective_discount > 0:
source_text = ""
if personal_dist >= promo_dist and personal_dist > 0:
source_text = "(персональная)"
elif promo_dist > 0:
source_text = "(промокод)"
msg += f"🔥 Скидка {effective_discount}% {source_text}\n"
msg += f"💰 Итого: <s>{plan['price']}</s> <b>{final_price} ⭐</b>"
else:
msg += f"💰 Цена: <b>{plan['price']} ⭐</b>"
kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💳 Оплатить", callback_data="pay_now")],
[InlineKeyboardButton(text="🎟 Ввести промокод" if promo_dist == 0 else "❌ Сбросить промокод", callback_data="enter_promo" if promo_dist == 0 else "reset_promo")],
[InlineKeyboardButton(text="◀️ Назад", callback_data="buy_subscription")]
])
if callback.message.photo:
await callback.message.delete()
await callback.message.answer(msg, reply_markup=kb, parse_mode="HTML")
else:
await callback.message.edit_text(msg, reply_markup=kb, parse_mode="HTML")
@router.callback_query(F.data == "enter_promo")
async def ask_promo(callback: CallbackQuery, state: FSMContext):
await callback.message.edit_text("Введите промокод:")
await state.set_state(PromoStates.waiting_for_promo)
@router.message(PromoStates.waiting_for_promo)
async def process_promo(message: Message, state: FSMContext):
promo_code = message.text.strip().upper()
promo = await db.get_promo_code(promo_code)
if promo:
discount = promo['discount']
is_unlimited = promo['is_unlimited']
bonus_days = promo['bonus_days']
is_sticky = promo['is_sticky']
await state.update_data(
promo_code=promo_code,
discount=discount,
is_unlimited_promo=is_unlimited,
bonus_days=bonus_days
)
# VIP Check
if is_unlimited:
await state.update_data(final_price=0, discount=100)
await message.answer(
f"🌟 VIP Промокод активирован!\n"
f"Вы получите БЕЗЛИМИТНЫЙ доступ.",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🚀 Активировать VIP", callback_data="pay_now")]
])
)
return
data = await state.get_data()
if 'selected_plan' in data:
plan_id = data['selected_plan']
plan = PLANS[plan_id]
# Recalculate with effective discount logic
user = await db.get_user(message.from_user.id)
personal_dist = user['personal_discount'] if user and user['personal_discount'] else 0
effective_discount = max(personal_dist, discount)
final_price = await calculate_final_price(plan['price'], effective_discount)
await state.update_data(final_price=final_price)
msg_text = ""
if bonus_days > 0 and discount == 0:
# Only bonus
msg_text += f"🎁 Бонус-код активирован! +{bonus_days} дней к тарифу.\n"
else:
msg_text += f"✅ Промокод на скидку {discount}% активирован!\n"
if is_sticky:
msg_text += "🔐 <b>Эта скидка закрепится за вами НАВСЕГДА после оплаты!</b>\n"
if personal_dist > discount:
msg_text += f"⚠️ У вас уже есть персональная скидка ({personal_dist}%), которая больше. Будет использована она."
msg_text += f"\nИтоговая цена: {final_price}"
await message.answer(
msg_text,
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="💳 Оплатить", callback_data="pay_now")],
[InlineKeyboardButton(text="◀️ Назад", callback_data="buy_subscription")],
])
)
else:
# Main menu activation logic
msg = f"✅ Промокод {promo_code} принят!"
if is_sticky:
msg += "\n🔐 Скидка будет закреплена за вами при следующей оплате."
await message.answer(
msg,
reply_markup=main_keyboard(message.from_user.id in CONFIG["ADMIN_IDS"])
)
else:
# Check context
data = await state.get_data()
kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Попробовать снова", callback_data="enter_promo")],
[InlineKeyboardButton(text="◀️ Отмена", callback_data="back_to_main")],
])
if 'selected_plan' in data:
kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="🔄 Попробовать снова", callback_data="enter_promo")],
[InlineKeyboardButton(text="💳 Оплатить без промокода", callback_data="pay_now")],
])
await message.answer("❌ Промокод недействителен, просрочен или исчерпан.", reply_markup=kb)
await state.set_state(None)
@router.callback_query(F.data == "use_promo")
async def use_promo_callback(callback: CallbackQuery, state: FSMContext):
text = "Введите промокод для активации скидки:"
kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="◀️ Отмена", callback_data="back_to_main")]
])
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)
await state.set_state(PromoStates.waiting_for_promo)
@router.callback_query(F.data == "reset_promo")
async def reset_promo(callback: CallbackQuery, state: FSMContext):
await state.update_data(promo_code=None, discount=None, final_price=None, is_unlimited_promo=False)
data = await state.get_data()
plan_id = data.get('selected_plan')
if plan_id:
plan = PLANS[plan_id]
await callback.message.edit_text(
f"Вы выбрали: {plan['name']}\n"
f"Стоимость: {plan['price']}\n\n"
f"📦 Трафик: {plan['data_limit']} ГБ\n"
f"⏰ Период: {plan['days']} дней\n\n"
"Есть промокод на скидку?",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="✅ Ввести промокод", callback_data="enter_promo")],
[InlineKeyboardButton(text="💳 Оплатить", callback_data="pay_now")],
[InlineKeyboardButton(text="◀️ Назад", callback_data="buy_subscription")],
])
)
else:
await show_plans(callback)
@router.callback_query(F.data == "help")
async def help_handler(callback: CallbackQuery):
help_text = (
" **Помощь и Инструкции**\n\n"
"**Как настроить VPN?**\n"
"1⃣ Нажмите «Моя подписка».\n"
"2⃣ Скопируйте ссылку-конфиг (начинается с `vless://`).\n"
"3⃣ Откройте приложение V2Ray/Hiddify и вставьте ссылку из буфера.\n"
"4⃣ Нажмите кнопку подключения (большая кнопка).\n\n"
"<EFBFBD> **Приложения для скачивания:**\n\n"
"🍏 **iOS (iPhone/iPad):**\n"
"• [V2Box](https://apps.apple.com/us/app/v2box-v2ray-client/id6446814690)\n"
"• [FoXray](https://apps.apple.com/us/app/foxray/id6448898396)\n"
"• [Shadowrocket](https://apps.apple.com/us/app/shadowrocket/id932747118) (Платное, но лучшее)\n\n"
"🤖 **Android:**\n"
"• [v2rayNG](https://play.google.com/store/apps/details?id=com.v2ray.ang)\n"
"• [Hiddify Next](https://play.google.com/store/apps/details?id=app.hiddify.com)\n\n"
"💻 **Windows:**\n"
"• [v2rayN](https://github.com/2dust/v2rayN/releases)\n"
"• [Hiddify Next](https://github.com/hiddify/hiddify-next/releases)\n\n"
"❓ **Проблемы?**\n"
"Если не подключается — попробуйте обновить подписку или напишите админу: @hoshimach1"
)
kb = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="◀️ Назад", callback_data="back_to_main")]
])
if callback.message.photo:
await callback.message.delete()
await callback.message.answer(help_text, reply_markup=kb, parse_mode="Markdown")
else:
await callback.message.edit_text(help_text, reply_markup=kb, parse_mode="Markdown", disable_web_page_preview=True)
@router.callback_query(F.data == "back_to_main")
async def back_to_main(callback: CallbackQuery):
user = await db.get_user(callback.from_user.id)
is_admin = callback.from_user.id in CONFIG["ADMIN_IDS"]
active = is_active(user)
text = "Главное меню:"
kb = main_keyboard(is_admin, active)
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)