Update WebApp

This commit is contained in:
2026-01-11 07:07:32 +03:00
parent 32d0f98a6e
commit 2b68dbac20
17 changed files with 3501 additions and 824 deletions

View File

@@ -148,42 +148,47 @@ 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 = "Нет активных промокодов."
text = "🏷 <b>Управление промокодами:</b>\n\n"
kb_buttons = []
now = datetime.now()
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
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 ""
# Получаем значения по ключам (не через get)
is_unl = p['is_unlimited']
type_str = " (VIP)" if is_unl else f" (-{p['discount']}%)"
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"
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 = 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")
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):
@@ -510,16 +515,18 @@ async def show_user_panel(message_or_call, 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}")]
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=f"adm_usr_msg_{user_id}")])
rows.append([InlineKeyboardButton(text="◀️ Назад", callback_data="admin_users_list")])
kb = InlineKeyboardMarkup(inline_keyboard=rows)
@@ -590,6 +597,109 @@ async def adm_usr_add_process(message: Message, state: FSMContext):
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):
@@ -673,14 +783,18 @@ async def adm_usr_limit_process(message: Message, state: FSMContext):
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)
# 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} GB")
await message.answer(f"✅ Лимит изменен на {limit_gb if limit_gb > 0 else ''} GB")
await show_user_panel(message, user_id)
await state.clear()
except ValueError:

View File

@@ -149,9 +149,10 @@ async def process_payment(callback: CallbackQuery, state: FSMContext):
await state.clear()
else:
# Создаем инвойс для Telegram Stars
limit_str = f"{plan['data_limit']} ГБ" if plan['data_limit'] > 0 else "Безлимит"
await callback.message.answer_invoice(
title=f"Подписка VPN - {plan['name']}",
description=f"Трафик: {plan['data_limit']} ГБ на {plan['days']} дней",
description=f"Трафик: {limit_str} на {plan['days']} дней",
payload=f"{plan_id}:{data.get('promo_code', '')}",
currency="XTR", # Telegram Stars
prices=[LabeledPrice(label=plan['name'], amount=final_price)],
@@ -206,10 +207,11 @@ async def successful_payment(message: Message):
bonus_days
)
limit_str = f"{plan['data_limit']} ГБ" if plan['data_limit'] > 0 else "Безлимит"
await message.answer(
f"✅ Оплата успешна!\n\n"
f"Ваша подписка активирована на {date_days} дней.\n"
f"Трафик: {plan['data_limit']} ГБ\n"
f"Трафик: {limit_str}\n"
f"{sticky_text}\n"
f"Получите конфигурацию через меню: 📊 Моя подписка",
reply_markup=main_keyboard(message.from_user.id in CONFIG["ADMIN_IDS"]),