commit ae3123535c5ca8b7e4c8b9899ee65eb969b5e711 Author: hoshimach1 Date: Thu Jan 8 03:09:23 2026 +0300 feat: Add main application file diff --git a/main.py b/main.py new file mode 100644 index 0000000..495837c --- /dev/null +++ b/main.py @@ -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()) \ No newline at end of file