Этот коммит содержится в:
stevefoxru
2025-04-24 14:11:40 +03:00
коммит произвёл GitHub
родитель 1218647c62
Коммит eac63031e9
+206 -505
Просмотреть файл
@@ -6,13 +6,7 @@ import aiofiles
import os import os
import re import re
import json import json
import subprocess
import sys import sys
import pytz
import zipfile
import ipaddress
import humanize
import shutil
import uuid import uuid
from aiogram import Bot, types from aiogram import Bot, types
from aiogram.dispatcher import Dispatcher from aiogram.dispatcher import Dispatcher
@@ -21,7 +15,6 @@ from aiogram.utils import executor
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from datetime import datetime, timedelta from datetime import datetime, timedelta
from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.interval import IntervalTrigger
from yoomoney import Client, Quickpay from yoomoney import Client, Quickpay
logging.basicConfig(level=logging.INFO) 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)) asyncio.create_task(delete_message_after_delay(message.chat.id, message.message_id))
dp = Dispatcher(bot) dp = Dispatcher(bot)
scheduler = AsyncIOScheduler(timezone=pytz.UTC) scheduler = AsyncIOScheduler(timezone=pytz.utc)
scheduler.start() scheduler.start()
dp.middleware.setup(AdminMessageDeletionMiddleware()) dp.middleware.setup(AdminMessageDeletionMiddleware())
@@ -80,13 +73,12 @@ def get_main_menu_markup(user_id):
) )
markup.add( markup.add(
InlineKeyboardButton("🔑 Получить конфиг", callback_data="get_config"), InlineKeyboardButton("🔑 Получить конфиг", callback_data="get_config"),
InlineKeyboardButton(" Инструкция", callback_data="instructions") InlineKeyboardButton("🎟 Управление промокодами", callback_data="manage_promocodes")
) )
markup.add( 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: elif user_id in moderators:
markup.add( markup.add(
InlineKeyboardButton("➕ Добавить пользователя", callback_data="add_user"), InlineKeyboardButton("➕ Добавить пользователя", callback_data="add_user"),
@@ -94,35 +86,43 @@ def get_main_menu_markup(user_id):
) )
markup.add( markup.add(
InlineKeyboardButton("🔑 Получить конфиг", callback_data="get_config"), InlineKeyboardButton("🔑 Получить конфиг", callback_data="get_config"),
InlineKeyboardButton("️ Инструкция", callback_data="instructions") InlineKeyboardButton("🏠 Домой", callback_data="home")
) )
else: else:
markup.add( markup.add(
InlineKeyboardButton("💳 Купить ключ", callback_data="buy_key"), InlineKeyboardButton("💳 Купить ключ", callback_data="buy_key"),
InlineKeyboardButton("🎟️ Использовать промокод", callback_data="use_promocode") InlineKeyboardButton("🎟️ Получить ключ по промокоду", callback_data="use_promocode")
) )
return markup 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(): def get_settings_menu():
markup = InlineKeyboardMarkup(row_width=2) markup = InlineKeyboardMarkup(row_width=2)
markup.add( markup.add(
InlineKeyboardButton("🔄 Проверить обновления", callback_data="check_updates"), InlineKeyboardButton("💾 Создать бэкап", callback_data="create_backup"),
InlineKeyboardButton("🔄 Перезагрузить VPN", callback_data="restart_vpn") InlineKeyboardButton("👥 Список админов", callback_data="list_admins")
) )
markup.add( markup.add(
InlineKeyboardButton("🗑️ Очистить старые ключи", callback_data="clear_old_keys"), InlineKeyboardButton("👤 Добавить админа", callback_data="add_admin"),
InlineKeyboardButton("💾 Создать бэкап", callback_data="create_backup") InlineKeyboardButton("💸 Настройки YooMoney", callback_data="yoomoney_settings")
) )
markup.add( markup.add(
InlineKeyboardButton("👥 Список админов", callback_data="list_admins"), InlineKeyboardButton("💰 Настройки цен", callback_data="pricing_settings"),
InlineKeyboardButton("👤 Добавить админа", callback_data="add_admin") 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 return markup
# Меню настроек YooMoney # Меню настроек YooMoney
@@ -159,65 +159,15 @@ def get_renewal_period_keyboard(username):
("1 месяц", "1_month"), ("1 месяц", "1_month"),
("3 месяца", "3_months"), ("3 месяца", "3_months"),
("6 месяцев", "6_months"), ("6 месяцев", "6_months"),
("12 месяцев", "12_months") ("12 месяцев", "12_months"),
("Кастомная дата", "custom_date")
] ]
for period_name, period_key in periods: for period_name, period_key in periods:
markup.add(InlineKeyboardButton(period_name, callback_data=f"renew_period_{username}_{period_key}")) markup.add(InlineKeyboardButton(period_name, callback_data=f"renew_period_{username}_{period_key}"))
markup.add(InlineKeyboardButton("Отмена", callback_data="home")) markup.add(InlineKeyboardButton("Отмена", callback_data="home"))
return markup 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 = {} 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): async def delete_message_after_delay(chat_id: int, message_id: int, delay: int = 2):
await asyncio.sleep(delay) await asyncio.sleep(delay)
@@ -226,40 +176,6 @@ async def delete_message_after_delay(chat_id: int, message_id: int, delay: int =
except: except:
pass 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: async def generate_vpn_key(conf_path: str) -> str:
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
'python3.11', '/root/amnezia-bot/awg/awg-decode.py', '--encode', conf_path, '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]}" username = f"user_{user_id}_{uuid.uuid4().hex[:8]}"
success = db.root_add(username, ipv6=False) success = db.root_add(username, ipv6=False)
if success: if success:
months = {'1_month': 1, '3_months': 3, '6_months': 6, '12_months': 12}[period] 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) expiration = datetime.now(pytz.utc) + timedelta(days=30 * months)
db.set_user_expiration(username, expiration, "Неограниченно") db.set_user_expiration(username, expiration, "Неограниченно")
db.set_user_telegram_id(username, user_id) db.set_user_telegram_id(username, user_id)
conf_path = os.path.join('users', username, f'{username}.conf') 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() @dp.message_handler()
async def handle_messages(message: types.Message): async def handle_messages(message: types.Message):
global PRICING # Объявляем PRICING глобальной в начале функции global PRICING
user_id = message.from_user.id user_id = message.from_user.id
user_state = user_main_messages.get(user_id, {}).get('state') 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 = message.text.strip()
promocode_data = db.apply_promocode(promocode) promocode_data = db.apply_promocode(promocode)
if promocode_data: if promocode_data:
discount = promocode_data.get('discount', 0)
subscription_period = promocode_data.get('subscription_period') subscription_period = promocode_data.get('subscription_period')
if subscription_period: if subscription_period:
success = await issue_vpn_key(user_id, subscription_period) success = await issue_vpn_key(user_id, subscription_period)
@@ -378,11 +293,25 @@ async def handle_messages(message: types.Message):
else: else:
await message.reply("Ошибка при выдаче ключа. Обратитесь к администратору.") await message.reply("Ошибка при выдаче ключа. Обратитесь к администратору.")
else: 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 user_main_messages[user_id]['promocode_discount'] = discount
await message.reply(f"Промокод активирован! Скидка: {discount}%") await message.reply(f"Промокод активирован! Скидка: {discount}%")
else: else:
await message.reply("Неверный или истёкший промокод.") 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] = { user_main_messages[user_id] = {
'chat_id': sent_message.chat.id, 'chat_id': sent_message.chat.id,
'message_id': sent_message.message_id, 'message_id': sent_message.message_id,
@@ -397,20 +326,21 @@ async def handle_messages(message: types.Message):
discount = float(discount) discount = float(discount)
days_valid = int(days_valid) days_valid = int(days_valid)
max_uses = int(max_uses) if max_uses.lower() != 'none' else None 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("Неверный период подписки") raise ValueError("Неверный период подписки")
subscription_period = None if subscription_period.lower() == 'none' else subscription_period 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): if db.add_promocode(code, discount, expires_at, max_uses, subscription_period):
await message.reply( await message.reply(
f"Промокод {code} добавлен: скидка {discount}%, действует {days_valid} дней, " f"Промокод {code} добавлен: скидка {discount}%, действует {days_valid} дней, "
f"макс. использований: {max_uses or 'неограничено'}, подписка: {subscription_period or 'нет'}" f"макс. использований: {max_uses or 'неограничено'}, период подписки: {subscription_period or 'нет'}"
) )
else: else:
await message.reply("Промокод уже существует.") await message.reply("Промокод уже существует.")
except: except:
await message.reply( 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)) sent_message = await message.answer("Выберите действие:", reply_markup=get_main_menu_markup(user_id))
user_main_messages[user_id] = { user_main_messages[user_id] = {
@@ -464,6 +394,24 @@ async def handle_messages(message: types.Message):
'message_id': sent_message.message_id, 'message_id': sent_message.message_id,
'state': None '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") @dp.callback_query_handler(lambda c: c.data == "settings")
async def settings_menu_callback(callback_query: types.CallbackQuery): 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() 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") @dp.callback_query_handler(lambda c: c.data == "add_user")
async def prompt_for_user_name(callback_query: types.CallbackQuery): async def prompt_for_user_name(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id user_id = callback_query.from_user.id
@@ -733,48 +630,25 @@ async def client_selected_callback(callback_query: types.CallbackQuery):
return return
status = "🔴 Офлайн" status = "🔴 Офлайн"
incoming_traffic = "↓—"
outgoing_traffic = "↑—"
ipv4_address = ""
expiration = db.get_user_expiration(username) expiration = db.get_user_expiration(username)
expiration_text = expiration.strftime("%Y-%m-%d %H:%M UTC") if expiration else "Не установлен" 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_clients = db.get_active_list()
active_info = next((ac for ac in active_clients if ac[0] == username), None) active_info = next((ac for ac in active_clients if ac[0] == username), None)
if active_info and active_info[1] and active_info[1].lower() not in ['never', 'нет данных', '-']:
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', 'нет данных', '-']:
try: try:
last_handshake = parse_relative_time(active_info[1]) 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 "❌ Офлайн" 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)}"
except: except:
pass pass
text = ( text = (
f"📧 *Имя:* {username}\n" f"📧 *Имя:* {username}\n"
f"👤 *Пользователь:* {telegram_id}\n"
f"🌐 *IPv4:* {ipv4_address}\n"
f"🌐 *Статус:* {status}\n" f"🌐 *Статус:* {status}\n"
f"🔼 *Исходящий:* {incoming_traffic}\n"
f"🔽 *Входящий:* {outgoing_traffic}\n"
f"⏰ *Срок действия:* {expiration_text}" f"⏰ *Срок действия:* {expiration_text}"
) )
keyboard = InlineKeyboardMarkup(row_width=2).add( 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"delete_user_{username}"),
InlineKeyboardButton("🔄 Продлить", callback_data=f"renew_user_{username}"), InlineKeyboardButton("🔄 Продлить", callback_data=f"renew_user_{username}"),
InlineKeyboardButton("⬅️ Назад", callback_data="list_users"), 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 bot.send_message(admin_id, "Вы удалены из администраторов.")
await list_admins_callback(callback_query) 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_')) @dp.callback_query_handler(lambda c: c.data.startswith('delete_user_'))
async def client_delete_callback(callback_query: types.CallbackQuery): async def client_delete_callback(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id user_id = callback_query.from_user.id
@@ -1080,7 +870,7 @@ async def renew_user_callback(callback_query: types.CallbackQuery):
pass pass
sent_message = await bot.send_message( sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id, chat_id=callback_query.message.chat.id,
text="Выберите период продления:", text="Выберите период продления или укажите дату:",
reply_markup=get_renewal_period_keyboard(username) reply_markup=get_renewal_period_keyboard(username)
) )
user_main_messages[user_id] = { 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) await callback_query.answer("Нет прав.", show_alert=True)
return return
try: try:
username, period = callback_query.data.split('renew_period_')[1].split('_', 1) parts = callback_query.data.split('renew_period_')[1].split('_', 1)
months = {'1_month': 1, '3_months': 3, '6_months': 6, '12_months': 12}[period] username = parts[0]
expiration = datetime.now(pytz.UTC) + timedelta(days=30 * months) period = parts[1] if len(parts) > 1 else 'custom_date'
db.set_user_expiration(username, expiration, "Неограниченно") if period == 'custom_date':
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)}")
try: try:
await bot.delete_message( await bot.delete_message(
chat_id=callback_query.message.chat.id, chat_id=callback_query.message.chat.id,
@@ -1113,6 +898,44 @@ async def renew_period_callback(callback_query: types.CallbackQuery):
) )
except: except:
pass 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( sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id, chat_id=callback_query.message.chat.id,
text=text, text=text,
@@ -1242,124 +1065,40 @@ async def create_backup_callback(callback_query: types.CallbackQuery):
} }
await callback_query.answer() 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") @dp.callback_query_handler(lambda c: c.data == "buy_key")
async def buy_key_callback(callback_query: types.CallbackQuery): 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 user_id = callback_query.from_user.id
keyboard = InlineKeyboardMarkup(row_width=2) keyboard = InlineKeyboardMarkup(row_width=2)
discount = user_main_messages.get(user_id, {}).get('promocode_discount', 0)
for period, price in PRICING.items(): for period, price in PRICING.items():
final_price = price * (1 - discount / 100)
keyboard.add(InlineKeyboardButton( keyboard.add(InlineKeyboardButton(
f"{period.replace('_', ' ')} - ₽{price:.2f}", f"{period.replace('_', ' ')} - ₽{final_price:.2f}{' (-' + str(discount) + '%)' if discount > 0 else ''}",
callback_data=f"select_period_{period}" callback_data=f"confirm_period_{period}"
)) ))
keyboard.add(InlineKeyboardButton("⬅️ Назад", callback_data="buy_key"))
keyboard.add(InlineKeyboardButton("🏠 Домой", callback_data="home")) keyboard.add(InlineKeyboardButton("🏠 Домой", callback_data="home"))
try: try:
await bot.delete_message( await bot.delete_message(
@@ -1380,10 +1119,10 @@ async def buy_key_callback(callback_query: types.CallbackQuery):
} }
await callback_query.answer() await callback_query.answer()
@dp.callback_query_handler(lambda c: c.data.startswith('select_period_')) @dp.callback_query_handler(lambda c: c.data.startswith('confirm_period_'))
async def select_period_callback(callback_query: types.CallbackQuery): async def confirm_period_callback(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id 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] price = PRICING[period]
discount = user_main_messages.get(user_id, {}).get('promocode_discount', 0) discount = user_main_messages.get(user_id, {}).get('promocode_discount', 0)
final_price = price * (1 - discount / 100) final_price = price * (1 - discount / 100)
@@ -1407,7 +1146,7 @@ async def select_period_callback(callback_query: types.CallbackQuery):
keyboard = InlineKeyboardMarkup().add( keyboard = InlineKeyboardMarkup().add(
InlineKeyboardButton("💳 Оплатить", url=payment_url), InlineKeyboardButton("💳 Оплатить", url=payment_url),
InlineKeyboardButton("⬅️ Назад", callback_data="buy_key"), InlineKeyboardButton("⬅️ Назад", callback_data="select_subscription_period"),
InlineKeyboardButton("🏠 Домой", callback_data="home") InlineKeyboardButton("🏠 Домой", callback_data="home")
) )
try: try:
@@ -1434,6 +1173,52 @@ async def select_period_callback(callback_query: types.CallbackQuery):
} }
await callback_query.answer() 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") @dp.callback_query_handler(lambda c: c.data == "use_promocode")
async def use_promocode_callback(callback_query: types.CallbackQuery): async def use_promocode_callback(callback_query: types.CallbackQuery):
user_id = callback_query.from_user.id user_id = callback_query.from_user.id
@@ -1446,7 +1231,7 @@ async def use_promocode_callback(callback_query: types.CallbackQuery):
pass pass
sent_message = await bot.send_message( sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id, chat_id=callback_query.message.chat.id,
text="Введите промокод:", text="Введите промокод для получения ключа:",
reply_markup=InlineKeyboardMarkup().add(InlineKeyboardButton("🏠 Домой", callback_data="home")) reply_markup=InlineKeyboardMarkup().add(InlineKeyboardButton("🏠 Домой", callback_data="home"))
) )
user_main_messages[user_id] = { user_main_messages[user_id] = {
@@ -1464,7 +1249,7 @@ async def manage_promocodes_callback(callback_query: types.CallbackQuery):
return return
promocodes = db.get_promocodes() promocodes = db.get_promocodes()
text = "Промокоды:\n" + "\n".join( 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() for code, info in promocodes.items()
) if promocodes else "Промокоды отсутствуют." ) if promocodes else "Промокоды отсутствуют."
keyboard = InlineKeyboardMarkup(row_width=2).add( keyboard = InlineKeyboardMarkup(row_width=2).add(
@@ -1506,7 +1291,7 @@ async def add_promocode_callback(callback_query: types.CallbackQuery):
pass pass
sent_message = await bot.send_message( sent_message = await bot.send_message(
chat_id=callback_query.message.chat.id, 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")) reply_markup=InlineKeyboardMarkup().add(InlineKeyboardButton("🏠 Домой", callback_data="home"))
) )
user_main_messages[user_id] = { 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 callback_query.answer(f"Промокод {code} не найден.", show_alert=True)
await manage_promocodes_callback(callback_query) await manage_promocodes_callback(callback_query)
@dp.callback_query_handler(lambda c: c.data == "check_updates") async def check_pending_payments():
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():
payments = db.get_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: try:
if yoomoney_client: history = yoomoney_client.operation_history(label=payment_id)
operation = yoomoney_client.operation_history(label=payment_id) for operation in history.operations:
for op in operation.operations: if operation.status == "success" and operation.amount == amount:
if op.label == payment_id and op.status == "success":
db.update_payment_status(payment_id, 'completed') 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) success = await issue_vpn_key(user_id, period)
if success: if success:
await bot.send_message(user_id, "Оплата подтверждена! Ваш VPN ключ отправлен.") await bot.send_message(user_id, f"Оплата подтверждена! VPN ключ на {period.replace('_', ' ')} выдан.")
else: else:
await bot.send_message(user_id, "Ошибка при создании пользователя. Обратитесь к администратору.") await bot.send_message(user_id, "Ошибка при выдаче ключа. Обратитесь к администратору.")
user_main_messages[user_id].pop('pending_payment', None)
break break
except Exception as e: 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__': if __name__ == '__main__':
import asyncio executor.start_polling(dp, skip_updates=True)
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()