feat: Add main application file
This commit is contained in:
771
main.py
Normal file
771
main.py
Normal file
@@ -0,0 +1,771 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
import json
|
||||
import random
|
||||
import string
|
||||
|
||||
from aiogram import Bot, Dispatcher, Router, F
|
||||
from aiogram.filters import Command, StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
from aiogram.types import (
|
||||
Message, CallbackQuery, InlineKeyboardMarkup,
|
||||
InlineKeyboardButton, LabeledPrice, PreCheckoutQuery
|
||||
)
|
||||
import aiohttp
|
||||
import asyncpg
|
||||
|
||||
# Конфигурация
|
||||
CONFIG = {
|
||||
"BOT_TOKEN": "YOUR_BOT_TOKEN",
|
||||
"MARZBAN_URL": "https://your-panel.com",
|
||||
"MARZBAN_USERNAME": "admin",
|
||||
"MARZBAN_PASSWORD": "your_password",
|
||||
"DATABASE_URL": "postgresql://user:password@localhost/vpnbot",
|
||||
"ADMIN_IDS": [123456789], # ID администраторов
|
||||
"PROVIDER_TOKEN": "", # Для Telegram Stars оставляем пустым
|
||||
}
|
||||
|
||||
# Тарифные планы
|
||||
PLANS = {
|
||||
"month_1": {"name": "1 месяц", "days": 30, "price": 100, "data_limit": 50},
|
||||
"month_3": {"name": "3 месяца", "days": 90, "price": 270, "data_limit": 150},
|
||||
"month_6": {"name": "6 месяцев", "days": 180, "price": 500, "data_limit": 300},
|
||||
}
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# FSM States
|
||||
class InviteStates(StatesGroup):
|
||||
waiting_for_code = State()
|
||||
|
||||
class PromoStates(StatesGroup):
|
||||
waiting_for_promo = State()
|
||||
creating_promo = State()
|
||||
promo_code = State()
|
||||
promo_discount = State()
|
||||
promo_uses = State()
|
||||
|
||||
class BroadcastStates(StatesGroup):
|
||||
waiting_for_message = State()
|
||||
|
||||
# Marzban API Client
|
||||
class MarzbanAPI:
|
||||
def __init__(self, url: str, username: str, password: str):
|
||||
self.url = url.rstrip('/')
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.token = None
|
||||
self.session = None
|
||||
|
||||
async def init_session(self):
|
||||
self.session = aiohttp.ClientSession()
|
||||
|
||||
async def close_session(self):
|
||||
if self.session:
|
||||
await self.session.close()
|
||||
|
||||
async def login(self):
|
||||
async with self.session.post(
|
||||
f"{self.url}/api/admin/token",
|
||||
data={"username": self.username, "password": self.password}
|
||||
) as resp:
|
||||
data = await resp.json()
|
||||
self.token = data["access_token"]
|
||||
return self.token
|
||||
|
||||
async def _request(self, method: str, endpoint: str, **kwargs):
|
||||
if not self.token:
|
||||
await self.login()
|
||||
|
||||
headers = {"Authorization": f"Bearer {self.token}"}
|
||||
async with self.session.request(
|
||||
method, f"{self.url}/api{endpoint}", headers=headers, **kwargs
|
||||
) as resp:
|
||||
if resp.status == 401:
|
||||
await self.login()
|
||||
headers = {"Authorization": f"Bearer {self.token}"}
|
||||
async with self.session.request(
|
||||
method, f"{self.url}/api{endpoint}", headers=headers, **kwargs
|
||||
) as retry_resp:
|
||||
return await retry_resp.json()
|
||||
return await resp.json()
|
||||
|
||||
async def create_user(self, username: str, data_limit: int, expire_days: int):
|
||||
expire_timestamp = int((datetime.now() + timedelta(days=expire_days)).timestamp())
|
||||
payload = {
|
||||
"username": username,
|
||||
"proxies": {
|
||||
"vless": {},
|
||||
"vmess": {}
|
||||
},
|
||||
"data_limit": data_limit * 1024 * 1024 * 1024, # GB to bytes
|
||||
"expire": expire_timestamp,
|
||||
"status": "active"
|
||||
}
|
||||
return await self._request("POST", "/user", json=payload)
|
||||
|
||||
async def get_user(self, username: str):
|
||||
return await self._request("GET", f"/user/{username}")
|
||||
|
||||
async def modify_user(self, username: str, data_limit: int, expire_days: int):
|
||||
expire_timestamp = int((datetime.now() + timedelta(days=expire_days)).timestamp())
|
||||
payload = {
|
||||
"data_limit": data_limit * 1024 * 1024 * 1024,
|
||||
"expire": expire_timestamp,
|
||||
"status": "active"
|
||||
}
|
||||
return await self._request("PUT", f"/user/{username}", json=payload)
|
||||
|
||||
async def delete_user(self, username: str):
|
||||
return await self._request("DELETE", f"/user/{username}")
|
||||
|
||||
async def get_system_stats(self):
|
||||
return await self._request("GET", "/system")
|
||||
|
||||
async def get_users_stats(self):
|
||||
return await self._request("GET", "/users")
|
||||
|
||||
# Database Manager
|
||||
class Database:
|
||||
def __init__(self, url: str):
|
||||
self.url = url
|
||||
self.pool = None
|
||||
|
||||
async def init_pool(self):
|
||||
self.pool = await asyncpg.create_pool(self.url)
|
||||
await self.create_tables()
|
||||
|
||||
async def create_tables(self):
|
||||
async with self.pool.acquire() as conn:
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
user_id BIGINT PRIMARY KEY,
|
||||
username TEXT,
|
||||
marzban_username TEXT UNIQUE,
|
||||
subscription_until TIMESTAMP,
|
||||
data_limit INTEGER,
|
||||
invited_by BIGINT,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
""")
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS invite_codes (
|
||||
code TEXT PRIMARY KEY,
|
||||
created_by BIGINT,
|
||||
used_by BIGINT,
|
||||
used_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
""")
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS promo_codes (
|
||||
code TEXT PRIMARY KEY,
|
||||
discount INTEGER,
|
||||
uses_left INTEGER,
|
||||
created_by BIGINT,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
""")
|
||||
await conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS payments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id BIGINT,
|
||||
plan TEXT,
|
||||
amount INTEGER,
|
||||
promo_code TEXT,
|
||||
paid_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
""")
|
||||
|
||||
async def get_user(self, user_id: int):
|
||||
async with self.pool.acquire() as conn:
|
||||
return await conn.fetchrow("SELECT * FROM users WHERE user_id = $1", user_id)
|
||||
|
||||
async def create_user(self, user_id: int, username: str, marzban_username: str, invited_by: int = None):
|
||||
async with self.pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"INSERT INTO users (user_id, username, marzban_username, invited_by) VALUES ($1, $2, $3, $4)",
|
||||
user_id, username, marzban_username, invited_by
|
||||
)
|
||||
|
||||
async def update_subscription(self, user_id: int, days: int, data_limit: int):
|
||||
async with self.pool.acquire() as conn:
|
||||
user = await self.get_user(user_id)
|
||||
if user and user['subscription_until'] and user['subscription_until'] > datetime.now():
|
||||
new_date = user['subscription_until'] + timedelta(days=days)
|
||||
else:
|
||||
new_date = datetime.now() + timedelta(days=days)
|
||||
|
||||
await conn.execute(
|
||||
"UPDATE users SET subscription_until = $1, data_limit = $2 WHERE user_id = $3",
|
||||
new_date, data_limit, user_id
|
||||
)
|
||||
|
||||
async def create_invite_code(self, created_by: int):
|
||||
code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
|
||||
async with self.pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"INSERT INTO invite_codes (code, created_by) VALUES ($1, $2)",
|
||||
code, created_by
|
||||
)
|
||||
return code
|
||||
|
||||
async def use_invite_code(self, code: str, user_id: int):
|
||||
async with self.pool.acquire() as conn:
|
||||
invite = await conn.fetchrow(
|
||||
"SELECT * FROM invite_codes WHERE code = $1 AND used_by IS NULL",
|
||||
code
|
||||
)
|
||||
if not invite:
|
||||
return False
|
||||
await conn.execute(
|
||||
"UPDATE invite_codes SET used_by = $1, used_at = NOW() WHERE code = $2",
|
||||
user_id, code
|
||||
)
|
||||
return invite['created_by']
|
||||
|
||||
async def create_promo_code(self, code: str, discount: int, uses: int, created_by: int):
|
||||
async with self.pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"INSERT INTO promo_codes (code, discount, uses_left, created_by) VALUES ($1, $2, $3, $4)",
|
||||
code, discount, uses, created_by
|
||||
)
|
||||
|
||||
async def use_promo_code(self, code: str):
|
||||
async with self.pool.acquire() as conn:
|
||||
promo = await conn.fetchrow(
|
||||
"SELECT * FROM promo_codes WHERE code = $1 AND uses_left > 0",
|
||||
code
|
||||
)
|
||||
if not promo:
|
||||
return None
|
||||
await conn.execute(
|
||||
"UPDATE promo_codes SET uses_left = uses_left - 1 WHERE code = $1",
|
||||
code
|
||||
)
|
||||
return promo['discount']
|
||||
|
||||
async def add_payment(self, user_id: int, plan: str, amount: int, promo_code: str = None):
|
||||
async with self.pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"INSERT INTO payments (user_id, plan, amount, promo_code) VALUES ($1, $2, $3, $4)",
|
||||
user_id, plan, amount, promo_code
|
||||
)
|
||||
|
||||
async def get_all_users(self):
|
||||
async with self.pool.acquire() as conn:
|
||||
return await conn.fetch("SELECT user_id FROM users")
|
||||
|
||||
async def get_stats(self):
|
||||
async with self.pool.acquire() as conn:
|
||||
total = await conn.fetchval("SELECT COUNT(*) FROM users")
|
||||
active = await conn.fetchval(
|
||||
"SELECT COUNT(*) FROM users WHERE subscription_until > NOW()"
|
||||
)
|
||||
revenue = await conn.fetchval("SELECT SUM(amount) FROM payments")
|
||||
return {"total": total, "active": active, "revenue": revenue or 0}
|
||||
|
||||
# Initialize
|
||||
bot = Bot(token=CONFIG["BOT_TOKEN"])
|
||||
storage = MemoryStorage()
|
||||
dp = Dispatcher(storage=storage)
|
||||
router = Router()
|
||||
dp.include_router(router)
|
||||
|
||||
marzban = MarzbanAPI(CONFIG["MARZBAN_URL"], CONFIG["MARZBAN_USERNAME"], CONFIG["MARZBAN_PASSWORD"])
|
||||
db = Database(CONFIG["DATABASE_URL"])
|
||||
|
||||
# Keyboards
|
||||
def main_keyboard(is_admin: bool = False):
|
||||
buttons = [
|
||||
[InlineKeyboardButton(text="📊 Моя подписка", callback_data="my_subscription")],
|
||||
[InlineKeyboardButton(text="💎 Купить подписку", callback_data="buy_subscription")],
|
||||
[InlineKeyboardButton(text="🎟️ Использовать промокод", callback_data="use_promo")],
|
||||
[InlineKeyboardButton(text="ℹ️ Помощь", callback_data="help")],
|
||||
]
|
||||
if is_admin:
|
||||
buttons.append([InlineKeyboardButton(text="👑 Админ-панель", callback_data="admin_panel")])
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
def admin_keyboard():
|
||||
return InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="📊 Статистика", callback_data="admin_stats")],
|
||||
[InlineKeyboardButton(text="🎟️ Создать промокод", callback_data="admin_create_promo")],
|
||||
[InlineKeyboardButton(text="👥 Создать инвайт", callback_data="admin_create_invite")],
|
||||
[InlineKeyboardButton(text="📢 Рассылка", callback_data="admin_broadcast")],
|
||||
[InlineKeyboardButton(text="🖥️ Статистика сервера", callback_data="admin_server_stats")],
|
||||
[InlineKeyboardButton(text="◀️ Назад", callback_data="back_to_main")],
|
||||
])
|
||||
|
||||
def plans_keyboard():
|
||||
buttons = []
|
||||
for plan_id, plan_data in PLANS.items():
|
||||
buttons.append([InlineKeyboardButton(
|
||||
text=f"{plan_data['name']} - {plan_data['price']} ⭐",
|
||||
callback_data=f"plan_{plan_id}"
|
||||
)])
|
||||
buttons.append([InlineKeyboardButton(text="◀️ Назад", callback_data="back_to_main")])
|
||||
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
||||
|
||||
# Handlers
|
||||
@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:
|
||||
is_admin = user_id in CONFIG["ADMIN_IDS"]
|
||||
await message.answer(
|
||||
f"Привет, {message.from_user.first_name}! 👋\n\n"
|
||||
"Выберите действие:",
|
||||
reply_markup=main_keyboard(is_admin)
|
||||
)
|
||||
else:
|
||||
await message.answer(
|
||||
"👋 Добро пожаловать!\n\n"
|
||||
"Для использования бота необходим инвайт-код.\n"
|
||||
"Введите ваш инвайт-код:"
|
||||
)
|
||||
await state.set_state(InviteStates.waiting_for_code)
|
||||
|
||||
@router.message(InviteStates.waiting_for_code)
|
||||
async def process_invite_code(message: Message, state: FSMContext):
|
||||
code = message.text.strip().upper()
|
||||
invited_by = await db.use_invite_code(code, message.from_user.id)
|
||||
|
||||
if not invited_by:
|
||||
await message.answer("❌ Неверный или использованный инвайт-код. Попробуйте снова:")
|
||||
return
|
||||
|
||||
marzban_username = f"user_{message.from_user.id}"
|
||||
await db.create_user(message.from_user.id, message.from_user.username, marzban_username, invited_by)
|
||||
|
||||
is_admin = message.from_user.id in CONFIG["ADMIN_IDS"]
|
||||
await message.answer(
|
||||
"✅ Регистрация успешна!\n\n"
|
||||
"Теперь вы можете приобрести подписку.",
|
||||
reply_markup=main_keyboard(is_admin)
|
||||
)
|
||||
await state.clear()
|
||||
|
||||
@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['subscription_until'] or user['subscription_until'] < datetime.now():
|
||||
await callback.message.edit_text(
|
||||
"❌ У вас нет активной подписки.\n\n"
|
||||
"Приобретите подписку, чтобы начать пользоваться VPN.",
|
||||
reply_markup=main_keyboard(callback.from_user.id in CONFIG["ADMIN_IDS"])
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
marzban_user = await marzban.get_user(user['marzban_username'])
|
||||
used_traffic = marzban_user.get('used_traffic', 0) / (1024**3) # bytes to GB
|
||||
|
||||
info_text = (
|
||||
f"📊 Ваша подписка:\n\n"
|
||||
f"⏰ Действует до: {user['subscription_until'].strftime('%d.%m.%Y %H:%M')}\n"
|
||||
f"📦 Лимит трафика: {user['data_limit']} ГБ\n"
|
||||
f"📊 Использовано: {used_traffic:.2f} ГБ\n\n"
|
||||
f"🔗 Конфигурация:\n"
|
||||
f"`{marzban_user.get('subscription_url', 'Генерируется...')}`"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user info: {e}")
|
||||
info_text = "⚠️ Ошибка получения данных. Попробуйте позже."
|
||||
|
||||
await callback.message.edit_text(
|
||||
info_text,
|
||||
reply_markup=main_keyboard(callback.from_user.id in CONFIG["ADMIN_IDS"]),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
|
||||
@router.callback_query(F.data == "buy_subscription")
|
||||
async def show_plans(callback: CallbackQuery):
|
||||
await callback.message.edit_text(
|
||||
"💎 Выберите тарифный план:\n\n"
|
||||
"Все планы включают:\n"
|
||||
"• Безлимитная скорость\n"
|
||||
"• Поддержка всех устройств\n"
|
||||
"• Техподдержка 24/7",
|
||||
reply_markup=plans_keyboard()
|
||||
)
|
||||
|
||||
@router.callback_query(F.data.startswith("plan_"))
|
||||
async def process_plan_selection(callback: CallbackQuery, state: FSMContext):
|
||||
plan_id = callback.data.split("_")[1]
|
||||
plan = PLANS[plan_id]
|
||||
|
||||
await state.update_data(selected_plan=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")],
|
||||
])
|
||||
)
|
||||
|
||||
@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()
|
||||
discount = await db.use_promo_code(promo_code)
|
||||
|
||||
data = await state.get_data()
|
||||
plan_id = data['selected_plan']
|
||||
plan = PLANS[plan_id]
|
||||
|
||||
if discount:
|
||||
new_price = int(plan['price'] * (100 - discount) / 100)
|
||||
await state.update_data(promo_code=promo_code, final_price=new_price)
|
||||
await message.answer(
|
||||
f"✅ Промокод применен! Скидка: {discount}%\n"
|
||||
f"Новая цена: {new_price} ⭐",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="💳 Оплатить", callback_data="pay_now")],
|
||||
[InlineKeyboardButton(text="◀️ Назад", callback_data="buy_subscription")],
|
||||
])
|
||||
)
|
||||
else:
|
||||
await message.answer(
|
||||
"❌ Промокод недействителен или исчерпан.",
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="🔄 Попробовать снова", callback_data="enter_promo")],
|
||||
[InlineKeyboardButton(text="💳 Оплатить без промокода", callback_data="pay_now")],
|
||||
])
|
||||
)
|
||||
await state.set_state(None)
|
||||
|
||||
@router.callback_query(F.data == "pay_now")
|
||||
async def process_payment(callback: CallbackQuery, state: FSMContext):
|
||||
data = await state.get_data()
|
||||
plan_id = data['selected_plan']
|
||||
plan = PLANS[plan_id]
|
||||
final_price = data.get('final_price', plan['price'])
|
||||
|
||||
# Создаем инвойс для 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 pre_checkout_handler(pre_checkout_query: PreCheckoutQuery):
|
||||
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(":")
|
||||
plan = PLANS[plan_id]
|
||||
|
||||
user = await db.get_user(message.from_user.id)
|
||||
|
||||
# Обновляем или создаем пользователя в Marzban
|
||||
try:
|
||||
if user['subscription_until'] and user['subscription_until'] > datetime.now():
|
||||
# Продлеваем существующую подписку
|
||||
await marzban.modify_user(
|
||||
user['marzban_username'],
|
||||
plan['data_limit'],
|
||||
plan['days']
|
||||
)
|
||||
else:
|
||||
# Создаем новую подписку
|
||||
await marzban.create_user(
|
||||
user['marzban_username'],
|
||||
plan['data_limit'],
|
||||
plan['days']
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Marzban error: {e}")
|
||||
|
||||
# Обновляем БД
|
||||
await db.update_subscription(message.from_user.id, plan['days'], plan['data_limit'])
|
||||
await db.add_payment(
|
||||
message.from_user.id,
|
||||
plan_id,
|
||||
payment.total_amount,
|
||||
promo_code if promo_code else None
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
f"✅ Оплата успешна!\n\n"
|
||||
f"Ваша подписка активирована на {plan['days']} дней.\n"
|
||||
f"Трафик: {plan['data_limit']} ГБ\n\n"
|
||||
f"Получите конфигурацию через: 📊 Моя подписка",
|
||||
reply_markup=main_keyboard(message.from_user.id in CONFIG["ADMIN_IDS"])
|
||||
)
|
||||
|
||||
# Admin handlers
|
||||
@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
|
||||
|
||||
await callback.message.edit_text(
|
||||
"👑 Панель администратора",
|
||||
reply_markup=admin_keyboard()
|
||||
)
|
||||
|
||||
@router.callback_query(F.data == "admin_stats")
|
||||
async def admin_stats(callback: CallbackQuery):
|
||||
if callback.from_user.id not in CONFIG["ADMIN_IDS"]:
|
||||
await callback.answer("❌ Доступ запрещен", show_alert=True)
|
||||
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"]:
|
||||
await callback.answer("❌ Доступ запрещен", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
system_stats = await marzban.get_system_stats()
|
||||
users_stats = await marzban.get_users_stats()
|
||||
|
||||
text = (
|
||||
"🖥️ Статистика сервера:\n\n"
|
||||
f"📊 CPU: {system_stats.get('cpu_usage', 'N/A')}%\n"
|
||||
f"💾 RAM: {system_stats.get('mem_used', 0) / (1024**3):.2f} / "
|
||||
f"{system_stats.get('mem_total', 0) / (1024**3):.2f} ГБ\n"
|
||||
f"💿 Диск: {system_stats.get('disk_used', 0) / (1024**3):.2f} / "
|
||||
f"{system_stats.get('disk_total', 0) / (1024**3):.2f} ГБ\n\n"
|
||||
f"👥 Пользователей в Marzban: {users_stats.get('total', 0)}\n"
|
||||
f"✅ Активных: {users_stats.get('active', 0)}\n"
|
||||
)
|
||||
except Exception as e:
|
||||
text = f"⚠️ Ошибка получения статистики: {str(e)}"
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=admin_keyboard())
|
||||
|
||||
@router.callback_query(F.data == "admin_create_invite")
|
||||
async def admin_create_invite(callback: CallbackQuery):
|
||||
if callback.from_user.id not in CONFIG["ADMIN_IDS"]:
|
||||
await callback.answer("❌ Доступ запрещен", show_alert=True)
|
||||
return
|
||||
|
||||
code = await db.create_invite_code(callback.from_user.id)
|
||||
await callback.answer(f"✅ Инвайт-код создан: {code}", show_alert=True)
|
||||
|
||||
@router.callback_query(F.data == "admin_create_promo")
|
||||
async def admin_create_promo_start(callback: CallbackQuery, state: FSMContext):
|
||||
if callback.from_user.id not in CONFIG["ADMIN_IDS"]:
|
||||
await callback.answer("❌ Доступ запрещен", show_alert=True)
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
"Создание промокода\n\n"
|
||||
"Введите код промокода (или 'авто' для генерации):"
|
||||
)
|
||||
await state.set_state(PromoStates.promo_code)
|
||||
|
||||
@router.message(PromoStates.promo_code)
|
||||
async def admin_promo_code(message: Message, state: FSMContext):
|
||||
code = message.text.strip().upper()
|
||||
if code == 'АВТО' or code == 'AUTO':
|
||||
code = ''.join(random.choices(string.ascii_uppercase + string.digits, k=8))
|
||||
|
||||
await state.update_data(promo_code=code)
|
||||
await message.answer("Введите размер скидки (%):")
|
||||
await state.set_state(PromoStates.promo_discount)
|
||||
|
||||
@router.message(PromoStates.promo_discount)
|
||||
async def admin_promo_discount(message: Message, state: FSMContext):
|
||||
try:
|
||||
discount = int(message.text.strip())
|
||||
if discount < 1 or discount > 100:
|
||||
await message.answer("❌ Скидка должна быть от 1 до 100%. Попробуйте снова:")
|
||||
return
|
||||
|
||||
await state.update_data(discount=discount)
|
||||
await message.answer("Введите количество использований:")
|
||||
await state.set_state(PromoStates.promo_uses)
|
||||
except ValueError:
|
||||
await message.answer("❌ Введите число. Попробуйте снова:")
|
||||
|
||||
@router.message(PromoStates.promo_uses)
|
||||
async def admin_promo_uses(message: Message, state: FSMContext):
|
||||
try:
|
||||
uses = int(message.text.strip())
|
||||
if uses < 1:
|
||||
await message.answer("❌ Минимум 1 использование. Попробуйте снова:")
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
await db.create_promo_code(
|
||||
data['promo_code'],
|
||||
data['discount'],
|
||||
uses,
|
||||
message.from_user.id
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
f"✅ Промокод создан!\n\n"
|
||||
f"Код: `{data['promo_code']}`\n"
|
||||
f"Скидка: {data['discount']}%\n"
|
||||
f"Использований: {uses}",
|
||||
reply_markup=admin_keyboard(),
|
||||
parse_mode="Markdown"
|
||||
)
|
||||
await state.clear()
|
||||
except ValueError:
|
||||
await message.answer("❌ Введите число. Попробуйте снова:")
|
||||
|
||||
@router.callback_query(F.data == "back_to_main")
|
||||
async def back_to_main(callback: CallbackQuery):
|
||||
is_admin = callback.from_user.id in CONFIG["ADMIN_IDS"]
|
||||
await callback.message.edit_text(
|
||||
"Главное меню:",
|
||||
reply_markup=main_keyboard(is_admin)
|
||||
)
|
||||
|
||||
@router.callback_query(F.data == "use_promo")
|
||||
async def use_promo_callback(callback: CallbackQuery):
|
||||
await callback.message.edit_text(
|
||||
"Эта функция доступна при покупке подписки.\n"
|
||||
"Сначала выберите тариф, затем можно будет ввести промокод.",
|
||||
reply_markup=main_keyboard(callback.from_user.id in CONFIG["ADMIN_IDS"])
|
||||
)
|
||||
|
||||
@router.callback_query(F.data == "help")
|
||||
async def help_handler(callback: CallbackQuery):
|
||||
help_text = (
|
||||
"ℹ️ Помощь по использованию бота:\n\n"
|
||||
"📱 Как подключиться:\n"
|
||||
"1. Купите подписку\n"
|
||||
"2. Получите ссылку конфигурации\n"
|
||||
"3. Скопируйте ссылку\n"
|
||||
"4. Вставьте в VPN-клиент\n\n"
|
||||
"📲 Рекомендуемые клиенты:\n"
|
||||
"• iOS: Shadowrocket, V2Box\n"
|
||||
"• Android: V2rayNG, NekoBox\n"
|
||||
"• Windows: v2rayN, Nekoray\n"
|
||||
"• macOS: V2rayU, ClashX\n\n"
|
||||
"❓ Возникли проблемы?\n"
|
||||
"Напишите администратору: @your_admin"
|
||||
)
|
||||
await callback.message.edit_text(
|
||||
help_text,
|
||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="◀️ Назад", callback_data="back_to_main")]
|
||||
])
|
||||
)
|
||||
|
||||
@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"]:
|
||||
await callback.answer("❌ Доступ запрещен", show_alert=True)
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
"📢 Рассылка сообщений\n\n"
|
||||
"Отправьте сообщение, которое хотите разослать всем пользователям:"
|
||||
)
|
||||
await state.set_state(BroadcastStates.waiting_for_message)
|
||||
|
||||
@router.message(BroadcastStates.waiting_for_message)
|
||||
async def admin_broadcast_send(message: Message, state: FSMContext):
|
||||
if message.from_user.id not in CONFIG["ADMIN_IDS"]:
|
||||
return
|
||||
|
||||
users = await db.get_all_users()
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
|
||||
status_msg = await message.answer("📤 Начинаю рассылку...")
|
||||
|
||||
for user in users:
|
||||
try:
|
||||
await bot.copy_message(
|
||||
chat_id=user['user_id'],
|
||||
from_chat_id=message.chat.id,
|
||||
message_id=message.message_id
|
||||
)
|
||||
success_count += 1
|
||||
await asyncio.sleep(0.05) # Защита от rate limit
|
||||
except Exception as e:
|
||||
fail_count += 1
|
||||
logger.error(f"Broadcast error for user {user['user_id']}: {e}")
|
||||
|
||||
await status_msg.edit_text(
|
||||
f"✅ Рассылка завершена!\n\n"
|
||||
f"Успешно: {success_count}\n"
|
||||
f"Ошибок: {fail_count}",
|
||||
reply_markup=admin_keyboard()
|
||||
)
|
||||
await state.clear()
|
||||
|
||||
# Команда для получения своего ID
|
||||
@router.message(Command("myid"))
|
||||
async def cmd_myid(message: Message):
|
||||
await message.answer(f"Ваш Telegram ID: `{message.from_user.id}`", parse_mode="Markdown")
|
||||
|
||||
# Обработка неизвестных команд
|
||||
@router.message()
|
||||
async def unknown_message(message: Message, state: FSMContext):
|
||||
current_state = await state.get_state()
|
||||
if current_state is None:
|
||||
user = await db.get_user(message.from_user.id)
|
||||
if user:
|
||||
is_admin = message.from_user.id in CONFIG["ADMIN_IDS"]
|
||||
await message.answer(
|
||||
"Используйте кнопки меню для навигации:",
|
||||
reply_markup=main_keyboard(is_admin)
|
||||
)
|
||||
else:
|
||||
await message.answer(
|
||||
"Для использования бота начните с команды /start"
|
||||
)
|
||||
|
||||
# Main function
|
||||
async def main():
|
||||
# Инициализация
|
||||
await db.init_pool()
|
||||
await marzban.init_session()
|
||||
|
||||
logger.info("Bot started successfully!")
|
||||
|
||||
try:
|
||||
await dp.start_polling(bot)
|
||||
finally:
|
||||
await marzban.close_session()
|
||||
await bot.session.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user