import asyncio import logging from datetime import datetime, timedelta from typing import Optional, Dict, Any import json import random import string from qrcode import QRCode import io 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, BufferedInputFile ) import aiohttp import aiosqlite from config import CONFIG, PLANS 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}"} url = f"{self.url}/api{endpoint}" logger.debug(f"Marzban Request: {method} {url} Payload: {kwargs.get('json')}") async with self.session.request( method, url, headers=headers, **kwargs ) as resp: data = await resp.json() logger.info(f"Marzban Response [{resp.status}]: {data}") if resp.status == 401: await self.login() headers = {"Authorization": f"Bearer {self.token}"} async with self.session.request( method, url, headers=headers, **kwargs ) as retry_resp: retry_data = await retry_resp.json() logger.info(f"Marzban Retry Response [{retry_resp.status}]: {retry_data}") return retry_data return data 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": {} }, "inbounds": {}, # Разрешить все входящие "excluded_inbounds": {}, # Ничего не исключать "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, "excluded_inbounds": {}, # Снимаем ограничения при продлении "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: Optional[str]): self.url = url self.is_sqlite = not url or not url.startswith("postgresql://") self.pool = None self.conn = None # For SQLite async def init_pool(self): if self.is_sqlite: db_path = "bot.db" self.conn = await aiosqlite.connect(db_path) self.conn.row_factory = aiosqlite.Row logger.info(f"Using SQLite database: {db_path}") else: self.pool = await asyncpg.create_pool(self.url) logger.info("Using PostgreSQL database") await self.create_tables() async def execute(self, query: str, *args): if self.is_sqlite: query = query.replace("$1", "?").replace("$2", "?").replace("$3", "?").replace("$4", "?") async with self.conn.execute(query, args) as cursor: await self.conn.commit() return cursor else: async with self.pool.acquire() as conn: return await conn.execute(query, *args) async def fetchrow(self, query: str, *args): if self.is_sqlite: query = query.replace("$1", "?").replace("$2", "?").replace("$3", "?").replace("$4", "?") async with self.conn.execute(query, args) as cursor: return await cursor.fetchone() else: async with self.pool.acquire() as conn: return await conn.fetchrow(query, *args) async def fetchval(self, query: str, *args): if self.is_sqlite: query = query.replace("$1", "?").replace("$2", "?").replace("$3", "?").replace("$4", "?") async with self.conn.execute(query, args) as cursor: row = await cursor.fetchone() return row[0] if row else None else: async with self.pool.acquire() as conn: return await conn.fetchval(query, *args) async def fetch(self, query: str, *args): if self.is_sqlite: query = query.replace("$1", "?").replace("$2", "?").replace("$3", "?").replace("$4", "?") async with self.conn.execute(query, args) as cursor: return await cursor.fetchall() else: async with self.pool.acquire() as conn: return await conn.fetch(query, *args) async def create_tables(self): now_default = "CURRENT_TIMESTAMP" if self.is_sqlite else "NOW()" serial_type = "INTEGER PRIMARY KEY AUTOINCREMENT" if self.is_sqlite else "SERIAL PRIMARY KEY" queries = [ f"""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_default} )""", f"""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_default} )""", f"""CREATE TABLE IF NOT EXISTS promo_codes ( code TEXT PRIMARY KEY, discount INTEGER, uses_left INTEGER, created_by BIGINT, created_at TIMESTAMP DEFAULT {now_default} )""", f"""CREATE TABLE IF NOT EXISTS payments ( id {serial_type}, user_id BIGINT, plan TEXT, amount INTEGER, promo_code TEXT, paid_at TIMESTAMP DEFAULT {now_default} )""" ] for q in queries: await self.execute(q) async def get_user(self, user_id: int): return await self.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): await self.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): user = await self.get_user(user_id) # SQLite returns Row object, datetime handling might need care sub_until = user['subscription_until'] if isinstance(sub_until, str): # SQLite might return it as string sub_until = datetime.strptime(sub_until, '%Y-%m-%d %H:%M:%S') if '.' not in sub_until else datetime.strptime(sub_until, '%Y-%m-%d %H:%M:%S.%f') if user and sub_until and sub_until > datetime.now(): new_date = sub_until + timedelta(days=days) else: new_date = datetime.now() + timedelta(days=days) await self.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)) await self.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): invite = await self.fetchrow( "SELECT * FROM invite_codes WHERE code = $1 AND used_by IS NULL", code ) if not invite: return False now_val = datetime.now() await self.execute( "UPDATE invite_codes SET used_by = $1, used_at = $2 WHERE code = $3", user_id, now_val, code ) return invite['created_by'] async def create_promo_code(self, code: str, discount: int, uses: int, created_by: int): await self.execute( "INSERT INTO promo_codes (code, discount, uses_left, created_by) VALUES ($1, $2, $3, $4)", code, discount, uses, created_by ) async def get_promo_code(self, code: str): return await self.fetchrow( "SELECT * FROM promo_codes WHERE code = $1 AND uses_left > 0", code ) async def decrement_promo_usage(self, code: str): await self.execute( "UPDATE promo_codes SET uses_left = uses_left - 1 WHERE code = $1", code ) async def add_payment(self, user_id: int, plan: str, amount: int, promo_code: str = None): await self.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): return await self.fetch("SELECT user_id FROM users") async def get_stats(self): now_expr = "CURRENT_TIMESTAMP" if self.is_sqlite else "NOW()" total = await self.fetchval("SELECT COUNT(*) FROM users") active = await self.fetchval( f"SELECT COUNT(*) FROM users WHERE subscription_until > {now_expr}" ) revenue = await self.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 not user and user_id in CONFIG["ADMIN_IDS"]: marzban_username = f"user_{user_id}" await db.create_user(user_id, message.from_user.username, marzban_username, invited_by=None) user = await db.get_user(user_id) await message.answer("✅ Администратор автоматически зарегистрирован.") 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) 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 with microseconds if previous format fails sub_until = datetime.strptime(sub_until, '%Y-%m-%d %H:%M:%S.%f') if not sub_until or sub_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']) # Если пользователя нет в панели, но подписка активна - пробуем создать заново 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...") await marzban.create_user( user['marzban_username'], user['data_limit'] or 50, # Лимит из базы или 50 по умолчанию 30 # Срок не важен, он обновится при следующей оплате ) 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_URL если он есть, иначе MARZBAN_URL base = CONFIG.get('BASE_URL') or CONFIG['MARZBAN_URL'] sub_url = f"{base.rstrip('/')}{sub_url}" info_text = ( f"📊 Ваша подписка:\n\n" f"⏰ Действует до: {sub_until.strftime('%d.%m.%Y %H:%M')}\n" f"📦 Лимит трафика: {user['data_limit']} ГБ\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 = "⚠️ Ошибка получения данных. Попробуйте позже." try: # Генерация QR-кода 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") # Если это текстовое сообщение - удаляем и шлем фото # Если это уже фото - пробуем edit_media (но проще удалить и прислать новое для чистоты) await callback.message.delete() await callback.message.answer_photo( photo=qr_file, caption=info_text, reply_markup=main_keyboard(callback.from_user.id in CONFIG["ADMIN_IDS"]), parse_mode="Markdown" ) except Exception as e: logger.error(f"Error sending subscription photo: {e}") # В случае ошибки - шлем текст как раньше await callback.message.answer(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): text = ( "💎 Выберите тарифный план:\n\n" "Все планы включают:\n" "• Безлимитная скорость\n" "• Поддержка всех устройств\n" "• Техподдержка 24/7" ) kb = plans_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.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() # Если промокод уже активирован if 'promo_code' in data and 'discount' in data: discount = data['discount'] new_price = int(plan['price'] * (100 - discount) / 100) await state.update_data(final_price=new_price) await callback.message.edit_text( f"Вы выбрали: {plan['name']}\n" f"Цена без скидки: {plan['price']} ⭐\n" f"✅ Применен промокод: {discount}%\n" f"Итого к оплате: {new_price} ⭐\n\n" f"📦 Трафик: {plan['data_limit']} ГБ\n" f"⏰ Период: {plan['days']} дней", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="💳 Оплатить", callback_data="pay_now")], [InlineKeyboardButton(text="❌ Сбросить промокод", callback_data="reset_promo")], [InlineKeyboardButton(text="◀️ Назад", callback_data="buy_subscription")], ]) ) else: 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() promo = await db.get_promo_code(promo_code) if promo: discount = promo['discount'] await state.update_data(promo_code=promo_code, discount=discount) data = await state.get_data() # Если план уже выбран (переход из покупки) if 'selected_plan' in data: plan_id = data['selected_plan'] plan = PLANS[plan_id] new_price = int(plan['price'] * (100 - discount) / 100) await state.update_data(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( f"✅ Промокод активирован! Скидка: {discount}%\n" "Теперь выберите тариф:", reply_markup=plans_keyboard() ) else: await message.answer( "❌ Промокод недействителен или исчерпан.", reply_markup=InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="🔄 Попробовать снова", callback_data="enter_promo")], [InlineKeyboardButton(text="◀️ Отмена", callback_data="back_to_main")], ]) ) await state.set_state(None) @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) # Перезагружаем выбор плана data = await state.get_data() if 'selected_plan' in data: # Имитируем повторный выбор плана для обновления текста callback.data = f"plan_{data['selected_plan']}" await process_plan_selection(callback, state) else: await back_to_main(callback) async def grant_subscription(user_id: int, plan_id: str, promo_code: str, amount: int): plan = PLANS[plan_id] user = await db.get_user(user_id) 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: sub_until = datetime.strptime(sub_until, '%Y-%m-%d %H:%M:%S.%f') # Marzban try: marzban_username = user['marzban_username'] resp = None if sub_until and sub_until > datetime.now(): logger.info(f"Attempting to modify existing user: {marzban_username}") resp = await marzban.modify_user( marzban_username, plan['data_limit'], plan['days'] ) # Если пользователь не найден в Marzban (хотя в БД бота он есть) 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, plan['data_limit'], plan['days'] ) else: logger.info(f"Creating/Reactivating user: {marzban_username}") resp = await marzban.create_user( marzban_username, plan['data_limit'], plan['days'] ) except Exception as e: logger.error(f"Marzban error in grant_subscription: {e}") # DB await db.update_subscription(user_id, plan['days'], plan['data_limit']) await db.add_payment( user_id, plan_id, amount, promo_code ) if promo_code: await db.decrement_promo_usage(promo_code) return plan @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') if not plan_id: await callback.answer("❌ Сессия истекла. Пожалуйста, выберите тариф снова.", show_alert=True) await show_plans(callback) return plan = PLANS[plan_id] final_price = int(data.get('final_price', plan['price'])) promo_code = data.get('promo_code') if final_price <= 0: await grant_subscription(callback.from_user.id, plan_id, promo_code, 0) await callback.message.edit_text( f"✅ Подписка активирована бесплатно!\n\n" f"План: {plan['name']}\n" f"Срок: {plan['days']} дней\n" f"Трафик: {plan['data_limit']} ГБ\n\n" f"Настройте подключение в меню: 📊 Моя подписка", reply_markup=main_keyboard(callback.from_user.id in CONFIG["ADMIN_IDS"]) ) await state.clear() return # Создаем инвойс для 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(":") if not promo_code: promo_code = None plan = await grant_subscription( message.from_user.id, plan_id, promo_code, payment.total_amount ) 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 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"]: 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"] text = "Главное меню:" kb = main_keyboard(is_admin) 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 == "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 == "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" "Напишите администратору: @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) else: await callback.message.edit_text(help_text, reply_markup=kb) @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"), StateFilter("*")) 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())