Files
marzban_tg_bot/main.py

799 lines
32 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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}"}
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: 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 use_promo_code(self, code: str):
promo = await self.fetchrow(
"SELECT * FROM promo_codes WHERE code = $1 AND uses_left > 0",
code
)
if not promo:
return None
await self.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):
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 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())