Этот коммит содержится в:
stevefoxru
2025-04-24 14:11:40 +03:00
коммит произвёл GitHub
родитель 1218647c62
Коммит eac63031e9
+206 -505
Просмотреть файл
@@ -6,13 +6,7 @@ import aiofiles
import os
import re
import json
import subprocess
import sys
import pytz
import zipfile
import ipaddress
import humanize
import shutil
import uuid
from aiogram import Bot, types
from aiogram.dispatcher import Dispatcher
@@ -21,7 +15,6 @@ from aiogram.utils import executor
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from datetime import datetime, timedelta
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.interval import IntervalTrigger
from yoomoney import Client, Quickpay
logging.basicConfig(level=logging.INFO)
@@ -66,7 +59,7 @@ class AdminMessageDeletionMiddleware(BaseMiddleware):
asyncio.create_task(delete_message_after_delay(message.chat.id, message.message_id))
dp = Dispatcher(bot)
scheduler = AsyncIOScheduler(timezone=pytz.UTC)
scheduler = AsyncIOScheduler(timezone=pytz.utc)
scheduler.start()
dp.middleware.setup(AdminMessageDeletionMiddleware())
@@ -80,13 +73,12 @@ def get_main_menu_markup(user_id):
)
markup.add(
InlineKeyboardButton("🔑 Получить конфиг", callback_data="get_config"),
InlineKeyboardButton(" Инструкция", callback_data="instructions")
InlineKeyboardButton("🎟 Управление промокодами", callback_data="manage_promocodes")
)
markup.add(
InlineKeyboardButton("🎟 Управление промокодами", callback_data="manage_promocodes"),
InlineKeyboardButton("⚙️ Настройки", callback_data="settings")
InlineKeyboardButton(" Настройки", callback_data="settings"),
InlineKeyboardButton("🏠 Домой", callback_data="home")
)
markup.add(InlineKeyboardButton("🏠 Домой", callback_data="home"))
elif user_id in moderators:
markup.add(
InlineKeyboardButton("➕ Добавить пользователя", callback_data="add_user"),
@@ -94,35 +86,43 @@ def get_main_menu_markup(user_id):
)
markup.add(
InlineKeyboardButton("🔑 Получить конфиг", callback_data="get_config"),
InlineKeyboardButton("️ Инструкция", callback_data="instructions")
InlineKeyboardButton("🏠 Домой", callback_data="home")
)
else:
markup.add(
InlineKeyboardButton("💳 Купить ключ", callback_data="buy_key"),
InlineKeyboardButton("🎟️ Использовать промокод", callback_data="use_promocode")
InlineKeyboardButton("🎟️ Получить ключ по промокоду", callback_data="use_promocode")
)
return markup
# Меню покупки ключа
def get_buy_key_menu(user_id):
markup = InlineKeyboardMarkup(row_width=2)
markup.add(
InlineKeyboardButton("📅 Выбрать период", callback_data="select_subscription_period"),
InlineKeyboardButton("🎟️ Ввести промокод", callback_data="enter_promocode_for_buy")
)
markup.add(InlineKeyboardButton("🏠 Домой", callback_data="home"))
discount = user_main_messages.get(user_id, {}).get('promocode_discount', 0)
if discount > 0:
markup.add(InlineKeyboardButton(f"Сбросить скидку ({discount}%)", callback_data="reset_promocode"))
return markup
# Меню настроек
def get_settings_menu():
markup = InlineKeyboardMarkup(row_width=2)
markup.add(
InlineKeyboardButton("🔄 Проверить обновления", callback_data="check_updates"),
InlineKeyboardButton("🔄 Перезагрузить VPN", callback_data="restart_vpn")
InlineKeyboardButton("💾 Создать бэкап", callback_data="create_backup"),
InlineKeyboardButton("👥 Список админов", callback_data="list_admins")
)
markup.add(
InlineKeyboardButton("🗑️ Очистить старые ключи", callback_data="clear_old_keys"),
InlineKeyboardButton("💾 Создать бэкап", callback_data="create_backup")
InlineKeyboardButton("👤 Добавить админа", callback_data="add_admin"),
InlineKeyboardButton("💸 Настройки YooMoney", callback_data="yoomoney_settings")
)
markup.add(
InlineKeyboardButton("👥 Список админов", callback_data="list_admins"),
InlineKeyboardButton("👤 Добавить админа", callback_data="add_admin")
InlineKeyboardButton("💰 Настройки цен", callback_data="pricing_settings"),
InlineKeyboardButton("⬅️ Назад", callback_data="home")
)
markup.add(
InlineKeyboardButton("💸 Настройки YooMoney", callback_data="yoomoney_settings"),
InlineKeyboardButton("💰 Настройки цен", callback_data="pricing_settings")
)
markup.add(InlineKeyboardButton("⬅️ Назад", callback_data="home"))
return markup
# Меню настроек YooMoney
@@ -159,65 +159,15 @@ def get_renewal_period_keyboard(username):
("1 месяц", "1_month"),
("3 месяца", "3_months"),
("6 месяцев", "6_months"),
("12 месяцев", "12_months")
("12 месяцев", "12_months"),
("Кастомная дата", "custom_date")
]
for period_name, period_key in periods:
markup.add(InlineKeyboardButton(period_name, callback_data=f"renew_period_{username}_{period_key}"))
markup.add(InlineKeyboardButton("Отмена", callback_data="home"))
return markup
# Клавиатура для выбора даты очистки ключей
def get_clear_keys_date_keyboard():
markup = InlineKeyboardMarkup(row_width=2)
dates = [
("1 месяц назад", (datetime.now(pytz.UTC) - timedelta(days=30)).isoformat()),
("3 месяца назад", (datetime.now(pytz.UTC) - timedelta(days=90)).isoformat()),
("6 месяцев назад", (datetime.now(pytz.UTC) - timedelta(days=180)).isoformat()),
("1 год назад", (datetime.now(pytz.UTC) - timedelta(days=365)).isoformat())
]
for date_name, date_iso in dates:
markup.add(InlineKeyboardButton(date_name, callback_data=f"clear_keys_date_{date_iso}"))
markup.add(InlineKeyboardButton("Отмена", callback_data="home"))
return markup
user_main_messages = {}
isp_cache = {}
ISP_CACHE_FILE = 'files/isp_cache.json'
CACHE_TTL = 24 * 3600
def get_interface_name():
return os.path.basename(WG_CONFIG_FILE).split('.')[0]
async def load_isp_cache():
global isp_cache
if os.path.exists(ISP_CACHE_FILE):
async with aiofiles.open(ISP_CACHE_FILE, 'r') as f:
isp_cache = json.loads(await f.read())
async def save_isp_cache():
async with aiofiles.open(ISP_CACHE_FILE, 'w') as f:
await f.write(json.dumps(isp_cache))
async def get_isp_info(ip: str) -> str:
now = datetime.now(pytz.UTC).timestamp()
if ip in isp_cache and (now - isp_cache[ip]['timestamp']) < CACHE_TTL:
return isp_cache[ip]['isp']
try:
if ipaddress.ip_address(ip).is_private:
return "Private Range"
except:
return "Invalid IP"
async with aiohttp.ClientSession() as session:
async with session.get(f"http://ip-api.com/json/{ip}?fields=isp") as resp:
if resp.status == 200:
data = await resp.json()
isp = data.get('isp', 'Unknown ISP')
isp_cache[ip] = {'isp': isp, 'timestamp': now}
await save_isp_cache()
return isp
return "Unknown ISP"
async def delete_message_after_delay(chat_id: int, message_id: int, delay: int = 2):
await asyncio.sleep(delay)
@@ -226,40 +176,6 @@ async def delete_message_after_delay(chat_id: int, message_id: int, delay: int =
except:
pass
def parse_relative_time(relative_str: str) -> datetime:
if not isinstance(relative_str, str) or not relative_str.strip():
logger.error(f"Некорректный relative_str: {relative_str}")
return datetime.now(pytz.UTC)
try:
relative_str = relative_str.lower().replace(' ago', '')
delta = 0
for part in relative_str.split(', '):
num, unit = part.split()
num = int(num)
if 'minute' in unit:
delta += num * 60
elif 'hour' in unit:
delta += num * 3600
elif 'day' in unit:
delta += num * 86400
elif 'week' in unit:
delta += num * 604800
elif 'month' in unit:
delta += num * 2592000
return datetime.now(pytz.UTC) - timedelta(seconds=delta)
except Exception as e:
logger.error(f"Ошибка в parse_relative_time: {str(e)}")
return datetime.now(pytz.UTC)
def parse_transfer(transfer_str: str) -> tuple:
try:
incoming, outgoing = transfer_str.split('/')
incoming_bytes = humanize.parse_bytes(incoming.strip())
outgoing_bytes = humanize.parse_bytes(outgoing.strip())
return incoming_bytes, outgoing_bytes
except:
return 0, 0
async def generate_vpn_key(conf_path: str) -> str:
process = await asyncio.create_subprocess_exec(
'python3.11', '/root/amnezia-bot/awg/awg-decode.py', '--encode', conf_path,
@@ -276,8 +192,8 @@ async def issue_vpn_key(user_id: int, period: str) -> bool:
username = f"user_{user_id}_{uuid.uuid4().hex[:8]}"
success = db.root_add(username, ipv6=False)
if success:
months = {'1_month': 1, '3_months': 3, '6_months': 6, '12_months': 12}[period]
expiration = datetime.now(pytz.UTC) + timedelta(days=30 * months)
months = {'1_month': 1, '3_months': 3, '6_months': 6, '12_months': 12}.get(period, 1)
expiration = datetime.now(pytz.utc) + timedelta(days=30 * months)
db.set_user_expiration(username, expiration, "Неограниченно")
db.set_user_telegram_id(username, user_id)
conf_path = os.path.join('users', username, f'{username}.conf')
@@ -325,7 +241,7 @@ async def add_admin_command(message: types.Message):
@dp.message_handler()
async def handle_messages(message: types.Message):
global PRICING # Объявляем PRICING глобальной в начале функции
global PRICING
user_id = message.from_user.id
user_state = user_main_messages.get(user_id, {}).get('state')
@@ -369,7 +285,6 @@ async def handle_messages(message: types.Message):
promocode = message.text.strip()
promocode_data = db.apply_promocode(promocode)
if promocode_data:
discount = promocode_data.get('discount', 0)
subscription_period = promocode_data.get('subscription_period')
if subscription_period:
success = await issue_vpn_key(user_id, subscription_period)
@@ -378,11 +293,25 @@ async def handle_messages(message: types.Message):
else:
await message.reply("Ошибка при выдаче ключа. Обратитесь к администратору.")
else:
await message.reply("Промокод не предоставляет ключ.")
else:
await message.reply("Неверный или истёкший промокод.")
sent_message = await message.answer("Выберите действие:", reply_markup=get_main_menu_markup(user_id))
user_main_messages[user_id] = {
'chat_id': sent_message.chat.id,
'message_id': sent_message.message_id,
'state': None
}
elif user_state == 'waiting_for_promocode_for_buy':
promocode = message.text.strip()
promocode_data = db.apply_promocode(promocode)
if promocode_data:
discount = promocode_data.get('discount', 0)
user_main_messages[user_id]['promocode_discount'] = discount
await message.reply(f"Промокод активирован! Скидка: {discount}%")
else:
await message.reply("Неверный или истёкший промокод.")
sent_message = await message.answer("Выберите действие:", reply_markup=get_main_menu_markup(user_id))
sent_message = await message.answer("Выберите действие:", reply_markup=get_buy_key_menu(user_id))
user_main_messages[user_id] = {
'chat_id': sent_message.chat.id,
'message_id': sent_message.message_id,
@@ -397,20 +326,21 @@ async def handle_messages(message: types.Message):
discount = float(discount)
days_valid = int(days_valid)
max_uses = int(max_uses) if max_uses.lower() != 'none' else None
if subscription_period not in PRICING and subscription_period.lower() != 'none':
if subscription_period not in ['none', '1_month', '3_months', '6_months', '12_months']:
raise ValueError("Неверный период подписки")
subscription_period = None if subscription_period.lower() == 'none' else subscription_period
expires_at = datetime.now(pytz.UTC) + timedelta(days=days_valid) if days_valid > 0 else None
expires_at = datetime.now(pytz.utc) + timedelta(days=days_valid) if days_valid > 0 else None
if db.add_promocode(code, discount, expires_at, max_uses, subscription_period):
await message.reply(
f"Промокод {code} добавлен: скидка {discount}%, действует {days_valid} дней, "
f"макс. использований: {max_uses or 'неограничено'}, подписка: {subscription_period or 'нет'}"
f"макс. использований: {max_uses or 'неограничено'}, период подписки: {subscription_period or 'нет'}"
)
else:
await message.reply("Промокод уже существует.")
except:
await message.reply(
"Формат: <код> <скидка%> <дней_действия> <макс_использований|none> <период_подписки|none>"
"Формат: <код> <скидка%> <дней_действия> <макс_использований|none> <период_подписки|none>\n"
"Пример: PROMO1 10 30 none 1_month"
)
sent_message = await message.answer("Выберите действие:", reply_markup=get_main_menu_markup(user_id))
user_main_messages[user_id] = {
@@ -464,6 +394,24 @@ async def handle_messages(message: types.Message):
'message_id': sent_message.message_id,
'state': None
}
elif user_state.startswith('waiting_for_custom_date_') and user_id in admins:
username = user_state.split('waiting_for_custom_date_')[1]
try:
expiration = datetime.strptime(message.text.strip(), '%d-%m-%Y').replace(tzinfo=pytz.utc)
if expiration < datetime.now(pytz.utc):
await message.reply("Дата должна быть в будущем.")
return
db.set_user_expiration(username, expiration, "Неограниченно")
await message.reply(f"Подписка для {username} продлена до {expiration.strftime('%d-%m-%Y')}.")
except:
await message.reply("Введите дату в формате ДД-ММ-ГГГГ (например, 31-12-2025).")
return
sent_message = await message.answer("Выберите действие:", reply_markup=get_main_menu_markup(user_id))
user_main_messages[user_id] = {
'chat_id': sent_message.chat.id,
'message_id': sent_message.message_id,
'state': None
}
@dp.callback_query_handler(lambda c: c.data == "settings")
async def settings_menu_callback(callback_query: types.CallbackQuery):
@@ -616,57 +564,6 @@ async def set_price_callback(callback_query: types.CallbackQuery):
}
await callback_query.answer()
@dp.callback_query_handler(lambda c: c.data == "clear_old_keys")
async def clear_old_keys_callback(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id
if user_id not in admins:
await callback_query.answer("Нет прав.", show_alert=True)
return
try:
await bot.delete_message(
chat_id=callback_query.message.chat.id,
message_id=callback_query.message.message_id
)
except:
pass
sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id,
text="Выберите дату, до которой удалить ключи:",
reply_markup=get_clear_keys_date_keyboard()
)
user_main_messages[user_id] = {
'chat_id': sent_message.chat.id,
'message_id': sent_message.message_id,
'state': None
}
await callback_query.answer()
@dp.callback_query_handler(lambda c: c.data.startswith('clear_keys_date_'))
async def clear_keys_date_callback(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id
if user_id not in admins:
await callback_query.answer("Нет прав.", show_alert=True)
return
before_date = callback_query.data.split('clear_keys_date_')[1]
try:
if db.clear_old_keys(before_date):
await bot.send_message(user_id, f"Старые ключи до {before_date} удалены.", parse_mode="Markdown")
else:
await bot.send_message(user_id, "Не найдено ключей для удаления.", parse_mode="Markdown")
except Exception as e:
await bot.send_message(user_id, f"Ошибка при удалении ключей: {str(e)}")
sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id,
text="Выберите действие:",
reply_markup=get_main_menu_markup(user_id)
)
user_main_messages[user_id] = {
'chat_id': sent_message.chat.id,
'message_id': sent_message.message_id,
'state': None
}
await callback_query.answer()
@dp.callback_query_handler(lambda c: c.data == "add_user")
async def prompt_for_user_name(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id
@@ -733,48 +630,25 @@ async def client_selected_callback(callback_query: types.CallbackQuery):
return
status = "🔴 Офлайн"
incoming_traffic = "↓—"
outgoing_traffic = "↑—"
ipv4_address = ""
expiration = db.get_user_expiration(username)
expiration_text = expiration.strftime("%Y-%m-%d %H:%M UTC") if expiration else "Не установлен"
telegram_id = db.get_user_telegram_id(username) or "Не указан"
if isinstance(client_info, (tuple, list)) and len(client_info) > 2 and client_info[2] is not None:
ip_match = re.search(r'(\d{1,3}\.){3}\d{1,3}/\d+', str(client_info[2]))
ipv4_address = ip_match.group(0) if ip_match else ""
active_clients = db.get_active_list()
active_info = next((ac for ac in active_clients if ac[0] == username), None)
if active_info and isinstance(active_info, (tuple, list)) and len(active_info) > 2:
if active_info[1] and active_info[1].lower() not in ['never', 'нет данных', '-']:
if active_info and active_info[1] and active_info[1].lower() not in ['never', 'нет данных', '-']:
try:
last_handshake = parse_relative_time(active_info[1])
status = "🟢 Онлайн" if (datetime.now(pytz.UTC) - last_handshake).total_seconds() <= 60 else "❌ Офлайн"
except:
pass
if active_info[2]:
try:
incoming_bytes, outgoing_bytes = parse_transfer(active_info[2])
incoming_traffic = f"{humanize.naturalsize(incoming_bytes)}"
outgoing_traffic = f"{humanize.naturalsize(outgoing_bytes)}"
last_handshake = datetime.strptime(active_info[1], "%Y-%m-%d %H:%M:%S")
status = "🟢 Онлайн" if (datetime.now(pytz.utc) - last_handshake).total_seconds() <= 60 else "❌ Офлайн"
except:
pass
text = (
f"📧 *Имя:* {username}\n"
f"👤 *Пользователь:* {telegram_id}\n"
f"🌐 *IPv4:* {ipv4_address}\n"
f"🌐 *Статус:* {status}\n"
f"🔼 *Исходящий:* {incoming_traffic}\n"
f"🔽 *Входящий:* {outgoing_traffic}\n"
f"⏰ *Срок действия:* {expiration_text}"
)
keyboard = InlineKeyboardMarkup(row_width=2).add(
InlineKeyboardButton("️ IP info", callback_data=f"ip_info_{username}"),
InlineKeyboardButton("🔗 Подключения", callback_data=f"connections_{username}"),
InlineKeyboardButton("🗑️ Удалить", callback_data=f"delete_user_{username}"),
InlineKeyboardButton("🔄 Продлить", callback_data=f"renew_user_{username}"),
InlineKeyboardButton("⬅️ Назад", callback_data="list_users"),
@@ -940,90 +814,6 @@ async def remove_admin_callback(callback_query: types.CallbackQuery):
await bot.send_message(admin_id, "Вы удалены из администраторов.")
await list_admins_callback(callback_query)
@dp.callback_query_handler(lambda c: c.data.startswith('connections_'))
async def client_connections_callback(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id
if user_id not in admins and user_id not in moderators:
await callback_query.answer("Нет прав.", show_alert=True)
return
username = callback_query.data.split('connections_')[1]
file_path = os.path.join('files', 'connections', f'{username}_ip.json')
if not os.path.exists(file_path):
await callback_query.answer("Нет данных о подключениях.", show_alert=True)
return
async with aiofiles.open(file_path, 'r') as f:
data = json.loads(await f.read())
last_connections = sorted(data.items(), key=lambda x: datetime.strptime(x[1], '%d.%m.%Y %H:%M'), reverse=True)[:5]
isp_results = await asyncio.gather(*(get_isp_info(ip) for ip, _ in last_connections))
text = f"*Последние подключения {username}:*\n" + "\n".join(f"{ip} ({isp}) - {time}" for (ip, time), isp in zip(last_connections, isp_results))
keyboard = InlineKeyboardMarkup(row_width=2).add(
InlineKeyboardButton("⬅️ Назад", callback_data=f"client_{username}"),
InlineKeyboardButton("🏠 Домой", callback_data="home")
)
try:
await bot.delete_message(
chat_id=callback_query.message.chat.id,
message_id=callback_query.message.message_id
)
except:
pass
sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id,
text=text,
parse_mode="Markdown",
reply_markup=keyboard
)
user_main_messages[user_id] = {
'chat_id': sent_message.chat.id,
'message_id': sent_message.message_id,
'state': None
}
await callback_query.answer()
@dp.callback_query_handler(lambda c: c.data.startswith('ip_info_'))
async def ip_info_callback(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id
if user_id not in admins and user_id not in moderators:
await callback_query.answer("Нет прав.", show_alert=True)
return
username = callback_query.data.split('ip_info_')[1]
active_info = next((ac for ac in db.get_active_list() if ac[0] == username), None)
if not active_info:
await callback_query.answer("Нет данных о подключении.", show_alert=True)
return
ip_address = active_info[3].split(':')[0]
async with aiohttp.ClientSession() as session:
async with session.get(f"http://ip-api.com/json/{ip_address}") as resp:
data = await resp.json() if resp.status == 200 else {}
text = f"*IP info {username}:*\n" + "\n".join(f"{k.capitalize()}: {v}" for k, v in data.items())
keyboard = InlineKeyboardMarkup(row_width=2).add(
InlineKeyboardButton("⬅️ Назад", callback_data=f"client_{username}"),
InlineKeyboardButton("🏠 Домой", callback_data="home")
)
try:
await bot.delete_message(
chat_id=callback_query.message.chat.id,
message_id=callback_query.message.message_id
)
except:
pass
sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id,
text=text,
parse_mode="Markdown",
reply_markup=keyboard
)
user_main_messages[user_id] = {
'chat_id': sent_message.chat.id,
'message_id': sent_message.message_id,
'state': None
}
await callback_query.answer()
@dp.callback_query_handler(lambda c: c.data.startswith('delete_user_'))
async def client_delete_callback(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id
@@ -1080,7 +870,7 @@ async def renew_user_callback(callback_query: types.CallbackQuery):
pass
sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id,
text="Выберите период продления:",
text="Выберите период продления или укажите дату:",
reply_markup=get_renewal_period_keyboard(username)
)
user_main_messages[user_id] = {
@@ -1097,15 +887,10 @@ async def renew_period_callback(callback_query: types.CallbackQuery):
await callback_query.answer("Нет прав.", show_alert=True)
return
try:
username, period = callback_query.data.split('renew_period_')[1].split('_', 1)
months = {'1_month': 1, '3_months': 3, '6_months': 6, '12_months': 12}[period]
expiration = datetime.now(pytz.UTC) + timedelta(days=30 * months)
db.set_user_expiration(username, expiration, "Неограниченно")
text = f"Подписка для {username} продлена до {expiration.strftime('%Y-%m-%d %H:%M UTC')}."
logger.info(f"Подписка для {username} продлена на {period} до {expiration}.")
except Exception as e:
text = f"Ошибка при продлении: {str(e)}"
logger.error(f"Ошибка при продлении подписки для {username}: {str(e)}")
parts = callback_query.data.split('renew_period_')[1].split('_', 1)
username = parts[0]
period = parts[1] if len(parts) > 1 else 'custom_date'
if period == 'custom_date':
try:
await bot.delete_message(
chat_id=callback_query.message.chat.id,
@@ -1113,6 +898,44 @@ async def renew_period_callback(callback_query: types.CallbackQuery):
)
except:
pass
sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id,
text="Введите дату продления в формате ДД-ММ-ГГГГ (например, 31-12-2025):",
reply_markup=InlineKeyboardMarkup().add(InlineKeyboardButton("Отмена", callback_data="home"))
)
user_main_messages[user_id] = {
'chat_id': sent_message.chat.id,
'message_id': sent_message.message_id,
'state': f'waiting_for_custom_date_{username}'
}
else:
months = {'1_month': 1, '3_months': 3, '6_months': 6, '12_months': 12}[period]
expiration = datetime.now(pytz.utc) + timedelta(days=30 * months)
db.set_user_expiration(username, expiration, "Неограниченно")
text = f"Подписка для {username} продлена до {expiration.strftime('%Y-%m-%d %H:%M UTC')}."
logger.info(f"Подписка для {username} продлена на {period} до {expiration}.")
try:
await bot.delete_message(
chat_id=callback_query.message.chat.id,
message_id=callback_query.message.message_id
)
except:
pass
sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id,
text=text,
parse_mode="Markdown",
reply_markup=get_main_menu_markup(user_id)
)
user_main_messages[user_id] = {
'chat_id': sent_message.chat.id,
'message_id': sent_message.message_id,
'state': None
}
await callback_query.answer()
except Exception as e:
text = f"Ошибка при продлении: {str(e)}"
logger.error(f"Ошибка при продлении подписки для {username}: {str(e)}")
sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id,
text=text,
@@ -1242,124 +1065,40 @@ async def create_backup_callback(callback_query: types.CallbackQuery):
}
await callback_query.answer()
@dp.callback_query_handler(lambda c: c.data == "instructions")
async def show_instructions(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id
if user_id not in admins and user_id not in moderators:
await callback_query.answer("Нет прав.", show_alert=True)
return
keyboard = InlineKeyboardMarkup(row_width=2).add(
InlineKeyboardButton("📱 Для мобильных", callback_data="mobile_instructions"),
InlineKeyboardButton("💻 Для компьютеров", callback_data="pc_instructions"),
InlineKeyboardButton("🏠 Домой", callback_data="home")
)
try:
await bot.delete_message(
chat_id=callback_query.message.chat.id,
message_id=callback_query.message.message_id
)
except:
pass
sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id,
text="Выберите тип устройства для инструкции:",
reply_markup=keyboard
)
user_main_messages[user_id] = {
'chat_id': sent_message.chat.id,
'message_id': sent_message.message_id,
'state': None
}
await callback_query.answer()
@dp.callback_query_handler(lambda c: c.data == "mobile_instructions")
async def mobile_instructions(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id
if user_id not in admins and user_id not in moderators:
await callback_query.answer("Нет прав.", show_alert=True)
return
instruction_text = (
"📱 *Инструкция для мобильных устройств:*\n\n"
"1. Скачайте приложение AmneziaVPN:\n"
" - [Google Play](https://play.google.com/store/apps/details?id=org.amnezia.vpn&hl=ru)\n"
" - Или через [GitHub](https://github.com/amnezia-vpn/amnezia-client)\n"
"2. Откройте приложение и выберите 'Добавить конфигурацию'.\n"
"3. Скопируйте VPN ключ из сообщения с файлом .conf.\n"
"4. Вставьте ключ в приложение и нажмите 'Подключить'.\n"
"5. Готово! Вы подключены к VPN."
)
keyboard = InlineKeyboardMarkup().add(
InlineKeyboardButton("⬅️ Назад", callback_data="instructions"),
InlineKeyboardButton("🏠 Домой", callback_data="home")
)
try:
await bot.delete_message(
chat_id=callback_query.message.chat.id,
message_id=callback_query.message.message_id
)
except:
pass
sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id,
text=instruction_text,
parse_mode="Markdown",
reply_markup=keyboard
)
user_main_messages[user_id] = {
'chat_id': sent_message.chat.id,
'message_id': sent_message.message_id,
'state': None
}
await callback_query.answer()
@dp.callback_query_handler(lambda c: c.data == "pc_instructions")
async def pc_instructions(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id
if user_id not in admins and user_id not in moderators:
await callback_query.answer("Нет прав.", show_alert=True)
return
instruction_text = (
"💻 *Инструкция для компьютеров:*\n\n"
"1. Скачайте клиент AmneziaVPN с [GitHub](https://github.com/amnezia-vpn/amnezia-client).\n"
"2. Установите программу на ваш компьютер.\n"
"3. Откройте AmneziaVPN и выберите 'Импорт конфигурации'.\n"
"4. Укажите путь к скачанному файлу .conf.\n"
"5. Нажмите 'Подключить' для активации VPN.\n"
"6. Готово! VPN активен."
)
keyboard = InlineKeyboardMarkup().add(
InlineKeyboardButton("⬅️ Назад", callback_data="instructions"),
InlineKeyboardButton("🏠 Домой", callback_data="home")
)
try:
await bot.delete_message(
chat_id=callback_query.message.chat.id,
message_id=callback_query.message.message_id
)
except:
pass
sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id,
text=instruction_text,
parse_mode="Markdown",
reply_markup=keyboard
)
user_main_messages[user_id] = {
'chat_id': sent_message.chat.id,
'message_id': sent_message.message_id,
'state': None
}
await callback_query.answer()
@dp.callback_query_handler(lambda c: c.data == "buy_key")
async def buy_key_callback(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id
try:
await bot.delete_message(
chat_id=callback_query.message.chat.id,
message_id=callback_query.message.message_id
)
except:
pass
sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id,
text="Меню покупки ключа:",
reply_markup=get_buy_key_menu(user_id)
)
user_main_messages[user_id] = {
'chat_id': sent_message.chat.id,
'message_id': sent_message.message_id,
'state': None
}
await callback_query.answer()
@dp.callback_query_handler(lambda c: c.data == "select_subscription_period")
async def select_period_callback(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id
keyboard = InlineKeyboardMarkup(row_width=2)
discount = user_main_messages.get(user_id, {}).get('promocode_discount', 0)
for period, price in PRICING.items():
final_price = price * (1 - discount / 100)
keyboard.add(InlineKeyboardButton(
f"{period.replace('_', ' ')} - ₽{price:.2f}",
callback_data=f"select_period_{period}"
f"{period.replace('_', ' ')} - ₽{final_price:.2f}{' (-' + str(discount) + '%)' if discount > 0 else ''}",
callback_data=f"confirm_period_{period}"
))
keyboard.add(InlineKeyboardButton("⬅️ Назад", callback_data="buy_key"))
keyboard.add(InlineKeyboardButton("🏠 Домой", callback_data="home"))
try:
await bot.delete_message(
@@ -1380,10 +1119,10 @@ async def buy_key_callback(callback_query: types.CallbackQuery):
}
await callback_query.answer()
@dp.callback_query_handler(lambda c: c.data.startswith('select_period_'))
async def select_period_callback(callback_query: types.CallbackQuery):
@dp.callback_query_handler(lambda c: c.data.startswith('confirm_period_'))
async def confirm_period_callback(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id
period = callback_query.data.split('select_period_')[1]
period = callback_query.data.split('confirm_period_')[1]
price = PRICING[period]
discount = user_main_messages.get(user_id, {}).get('promocode_discount', 0)
final_price = price * (1 - discount / 100)
@@ -1407,7 +1146,7 @@ async def select_period_callback(callback_query: types.CallbackQuery):
keyboard = InlineKeyboardMarkup().add(
InlineKeyboardButton("💳 Оплатить", url=payment_url),
InlineKeyboardButton("⬅️ Назад", callback_data="buy_key"),
InlineKeyboardButton("⬅️ Назад", callback_data="select_subscription_period"),
InlineKeyboardButton("🏠 Домой", callback_data="home")
)
try:
@@ -1434,6 +1173,52 @@ async def select_period_callback(callback_query: types.CallbackQuery):
}
await callback_query.answer()
@dp.callback_query_handler(lambda c: c.data == "enter_promocode_for_buy")
async def enter_promocode_for_buy_callback(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id
try:
await bot.delete_message(
chat_id=callback_query.message.chat.id,
message_id=callback_query.message.message_id
)
except:
pass
sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id,
text="Введите промокод для скидки:",
reply_markup=InlineKeyboardMarkup().add(InlineKeyboardButton("⬅️ Назад", callback_data="buy_key"))
)
user_main_messages[user_id] = {
'chat_id': sent_message.chat.id,
'message_id': sent_message.message_id,
'state': 'waiting_for_promocode_for_buy'
}
await callback_query.answer()
@dp.callback_query_handler(lambda c: c.data == "reset_promocode")
async def reset_promocode_callback(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id
if 'promocode_discount' in user_main_messages.get(user_id, {}):
del user_main_messages[user_id]['promocode_discount']
try:
await bot.delete_message(
chat_id=callback_query.message.chat.id,
message_id=callback_query.message.message_id
)
except:
pass
sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id,
text="Скидка сброшена. Меню покупки ключа:",
reply_markup=get_buy_key_menu(user_id)
)
user_main_messages[user_id] = {
'chat_id': sent_message.chat.id,
'message_id': sent_message.message_id,
'state': None
}
await callback_query.answer()
@dp.callback_query_handler(lambda c: c.data == "use_promocode")
async def use_promocode_callback(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id
@@ -1446,7 +1231,7 @@ async def use_promocode_callback(callback_query: types.CallbackQuery):
pass
sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id,
text="Введите промокод:",
text="Введите промокод для получения ключа:",
reply_markup=InlineKeyboardMarkup().add(InlineKeyboardButton("🏠 Домой", callback_data="home"))
)
user_main_messages[user_id] = {
@@ -1464,7 +1249,7 @@ async def manage_promocodes_callback(callback_query: types.CallbackQuery):
return
promocodes = db.get_promocodes()
text = "Промокоды:\n" + "\n".join(
f"{code}: {info['discount']}% (использовано {info['uses']}/{info['max_uses'] or ''}, до {info['expires_at'] or 'неограничено'}, подписка: {info['subscription_period'] or 'нет'})"
f"{code}: {info['discount']}% (использовано {info['uses']}/{info['max_uses'] or ''}, до {info['expires_at'].strftime('%Y-%m-%d %H:%M UTC') if info['expires_at'] else 'неограничено'}, период подписки: {info['subscription_period'] or 'нет'})"
for code, info in promocodes.items()
) if promocodes else "Промокоды отсутствуют."
keyboard = InlineKeyboardMarkup(row_width=2).add(
@@ -1506,7 +1291,7 @@ async def add_promocode_callback(callback_query: types.CallbackQuery):
pass
sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id,
text="Введите промокод в формате: <код> <скидка%> <дней_действия> <макс_использований|none> <период_подписки|none>",
text="Введите промокод в формате: <код> <скидка%> <дней_действия> <макс_использований|none> <период_подписки|none>\nПример: PROMO1 10 30 none 1_month",
reply_markup=InlineKeyboardMarkup().add(InlineKeyboardButton("🏠 Домой", callback_data="home"))
)
user_main_messages[user_id] = {
@@ -1559,109 +1344,25 @@ async def remove_promocode_callback(callback_query: types.CallbackQuery):
await callback_query.answer(f"Промокод {code} не найден.", show_alert=True)
await manage_promocodes_callback(callback_query)
@dp.callback_query_handler(lambda c: c.data == "check_updates")
async def check_updates_callback(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id
if user_id not in admins:
await callback_query.answer("Нет прав.", show_alert=True)
return
try:
process = await asyncio.create_subprocess_exec(
'/root/install.sh', '--check-update',
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
output = stdout.decode().strip() + stderr.decode().strip()
if "Репозиторий актуален" in output:
await bot.send_message(user_id, "Репозиторий актуален, обновления не требуются.", parse_mode="Markdown")
elif "Обновление репозитория... Done!" in output:
await bot.send_message(user_id, "Репозиторий успешно обновлён и служба перезапущена.", parse_mode="Markdown")
else:
await bot.send_message(user_id, f"Ошибка проверки обновлений:\n```\n{output}\n```", parse_mode="Markdown")
except Exception as e:
await bot.send_message(user_id, f"Ошибка при проверке обновлений: {str(e)}")
sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id,
text="Выберите действие:",
reply_markup=get_main_menu_markup(user_id)
)
user_main_messages[user_id] = {
'chat_id': sent_message.chat.id,
'message_id': sent_message.message_id,
'state': None
}
await callback_query.answer()
@dp.callback_query_handler(lambda c: c.data == "restart_vpn")
async def restart_vpn_callback(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id
if user_id not in admins:
await callback_query.answer("Нет прав.", show_alert=True)
return
try:
process = await asyncio.create_subprocess_exec(
'docker', 'ps', '-q', '-f', f'name={DOCKER_CONTAINER}',
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if not stdout.decode().strip():
await bot.send_message(user_id, f"Контейнер {DOCKER_CONTAINER} не найден.", parse_mode="Markdown")
else:
process = await asyncio.create_subprocess_exec(
'docker', 'restart', DOCKER_CONTAINER,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if process.returncode == 0:
await bot.send_message(user_id, f"VPN-контейнер {DOCKER_CONTAINER} успешно перезапущен.", parse_mode="Markdown")
else:
await bot.send_message(user_id, f"Ошибка при перезапуске VPN:\n```\n{stderr.decode().strip()}\n```", parse_mode="Markdown")
except Exception as e:
await bot.send_message(user_id, f"Ошибка при перезапуске VPN: {str(e)}")
sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id,
text="Выберите действие:",
reply_markup=get_main_menu_markup(user_id)
)
user_main_messages[user_id] = {
'chat_id': sent_message.chat.id,
'message_id': sent_message.message_id,
'state': None
}
await callback_query.answer()
async def check_payment_status():
async def check_pending_payments():
payments = db.get_pending_payments()
for user_id, payment_id, amount, _ in payments:
for payment in payments:
user_id, payment_id, amount, period = payment
try:
if yoomoney_client:
operation = yoomoney_client.operation_history(label=payment_id)
for op in operation.operations:
if op.label == payment_id and op.status == "success":
history = yoomoney_client.operation_history(label=payment_id)
for operation in history.operations:
if operation.status == "success" and operation.amount == amount:
db.update_payment_status(payment_id, 'completed')
pending_payment = user_main_messages.get(user_id, {}).get('pending_payment', {})
if pending_payment and pending_payment['payment_id'] == payment_id:
period = pending_payment['period']
success = await issue_vpn_key(user_id, period)
if success:
await bot.send_message(user_id, "Оплата подтверждена! Ваш VPN ключ отправлен.")
await bot.send_message(user_id, f"Оплата подтверждена! VPN ключ на {period.replace('_', ' ')} выдан.")
else:
await bot.send_message(user_id, "Ошибка при создании пользователя. Обратитесь к администратору.")
user_main_messages[user_id].pop('pending_payment', None)
await bot.send_message(user_id, "Ошибка при выдаче ключа. Обратитесь к администратору.")
break
except Exception as e:
logger.error(f"Ошибка проверки платежа {payment_id}: {str(e)}")
logger.error(f"Ошибка при проверке платежа {payment_id}: {str(e)}")
scheduler.add_job(check_pending_payments, IntervalTrigger(minutes=5))
if __name__ == '__main__':
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(load_isp_cache())
scheduler.add_job(check_payment_status, IntervalTrigger(minutes=5))
executor.start_polling(dp, skip_updates=True, loop=loop)
finally:
loop.close()
executor.start_polling(dp, skip_updates=True)