Update bot_manager.py
Этот коммит содержится в:
+260
-64
@@ -13,6 +13,7 @@ import zipfile
|
|||||||
import ipaddress
|
import ipaddress
|
||||||
import humanize
|
import humanize
|
||||||
import shutil
|
import shutil
|
||||||
|
import uuid
|
||||||
from aiogram import Bot, types
|
from aiogram import Bot, types
|
||||||
from aiogram.dispatcher import Dispatcher
|
from aiogram.dispatcher import Dispatcher
|
||||||
from aiogram.dispatcher.middlewares import BaseMiddleware
|
from aiogram.dispatcher.middlewares import BaseMiddleware
|
||||||
@@ -45,6 +46,13 @@ WG_CONFIG_FILE = wg_config_file
|
|||||||
DOCKER_CONTAINER = docker_container
|
DOCKER_CONTAINER = docker_container
|
||||||
ENDPOINT = endpoint
|
ENDPOINT = endpoint
|
||||||
|
|
||||||
|
# Цены за периоды (в условных единицах)
|
||||||
|
PRICING = {
|
||||||
|
'1_month': 10.0,
|
||||||
|
'3_months': 25.0,
|
||||||
|
'6_months': 45.0
|
||||||
|
}
|
||||||
|
|
||||||
class AdminMessageDeletionMiddleware(BaseMiddleware):
|
class AdminMessageDeletionMiddleware(BaseMiddleware):
|
||||||
async def on_process_message(self, message: types.Message, data: dict):
|
async def on_process_message(self, message: types.Message, data: dict):
|
||||||
if message.from_user.id in admins and message.text.startswith('/'):
|
if message.from_user.id in admins and message.text.startswith('/'):
|
||||||
@@ -55,10 +63,9 @@ scheduler = AsyncIOScheduler(timezone=pytz.UTC)
|
|||||||
scheduler.start()
|
scheduler.start()
|
||||||
dp.middleware.setup(AdminMessageDeletionMiddleware())
|
dp.middleware.setup(AdminMessageDeletionMiddleware())
|
||||||
|
|
||||||
# Главное меню с новым порядком и эмодзи
|
# Главное меню
|
||||||
def get_main_menu_markup(user_id):
|
def get_main_menu_markup(user_id):
|
||||||
markup = InlineKeyboardMarkup(row_width=2)
|
markup = InlineKeyboardMarkup(row_width=2)
|
||||||
# Последовательное добавление кнопок с помощью add
|
|
||||||
markup.add(
|
markup.add(
|
||||||
InlineKeyboardButton("➕ Добавить пользователя", callback_data="add_user"),
|
InlineKeyboardButton("➕ Добавить пользователя", callback_data="add_user"),
|
||||||
InlineKeyboardButton("📋 Список клиентов", callback_data="list_users")
|
InlineKeyboardButton("📋 Список клиентов", callback_data="list_users")
|
||||||
@@ -67,12 +74,19 @@ def get_main_menu_markup(user_id):
|
|||||||
InlineKeyboardButton("🔑 Получить конфиг", callback_data="get_config"),
|
InlineKeyboardButton("🔑 Получить конфиг", callback_data="get_config"),
|
||||||
InlineKeyboardButton("ℹ️ Инструкция", callback_data="instructions")
|
InlineKeyboardButton("ℹ️ Инструкция", callback_data="instructions")
|
||||||
)
|
)
|
||||||
|
markup.add(
|
||||||
|
InlineKeyboardButton("💳 Купить ключ", callback_data="buy_key"),
|
||||||
|
InlineKeyboardButton("🎟️ Использовать промокод", callback_data="use_promocode")
|
||||||
|
)
|
||||||
if user_id in admins:
|
if user_id in admins:
|
||||||
markup.add(
|
markup.add(
|
||||||
InlineKeyboardButton("👥 Список админов", callback_data="list_admins"),
|
InlineKeyboardButton("👥 Список админов", callback_data="list_admins"),
|
||||||
InlineKeyboardButton("👤 Добавить админа", callback_data="add_admin")
|
InlineKeyboardButton("👤 Добавить админа", callback_data="add_admin")
|
||||||
)
|
)
|
||||||
markup.add(InlineKeyboardButton("💾 Создать бекап", callback_data="create_backup"))
|
markup.add(
|
||||||
|
InlineKeyboardButton("💾 Создать бекап", callback_data="create_backup"),
|
||||||
|
InlineKeyboardButton("🎟️ Управление промокодами", callback_data="manage_promocodes")
|
||||||
|
)
|
||||||
return markup
|
return markup
|
||||||
|
|
||||||
user_main_messages = {}
|
user_main_messages = {}
|
||||||
@@ -124,7 +138,7 @@ async def delete_message_after_delay(chat_id: int, message_id: int, delay: int =
|
|||||||
def parse_relative_time(relative_str: str) -> datetime:
|
def parse_relative_time(relative_str: str) -> datetime:
|
||||||
if not isinstance(relative_str, str) or not relative_str.strip():
|
if not isinstance(relative_str, str) or not relative_str.strip():
|
||||||
logger.error(f"Некорректный relative_str: {relative_str}")
|
logger.error(f"Некорректный relative_str: {relative_str}")
|
||||||
return datetime.now(pytz.UTC) # Значение по умолчанию
|
return datetime.now(pytz.UTC)
|
||||||
try:
|
try:
|
||||||
relative_str = relative_str.lower().replace(' ago', '')
|
relative_str = relative_str.lower().replace(' ago', '')
|
||||||
delta = 0
|
delta = 0
|
||||||
@@ -144,7 +158,7 @@ def parse_relative_time(relative_str: str) -> datetime:
|
|||||||
return datetime.now(pytz.UTC) - timedelta(seconds=delta)
|
return datetime.now(pytz.UTC) - timedelta(seconds=delta)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка в parse_relative_time: {str(e)}")
|
logger.error(f"Ошибка в parse_relative_time: {str(e)}")
|
||||||
return datetime.now(pytz.UTC) # Значение по умолчанию
|
return datetime.now(pytz.UTC)
|
||||||
|
|
||||||
@dp.message_handler(commands=['start', 'help'])
|
@dp.message_handler(commands=['start', 'help'])
|
||||||
async def help_command_handler(message: types.Message):
|
async def help_command_handler(message: types.Message):
|
||||||
@@ -154,10 +168,18 @@ async def help_command_handler(message: types.Message):
|
|||||||
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,
|
||||||
'state': None # Инициализируем state явно
|
'state': None
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
await message.answer("У вас нет доступа к этому боту.")
|
sent_message = await message.answer("Добро пожаловать! Купите ключ для доступа к VPN.", reply_markup=InlineKeyboardMarkup().add(
|
||||||
|
InlineKeyboardButton("💳 Купить ключ", callback_data="buy_key"),
|
||||||
|
InlineKeyboardButton("🎟️ Использовать промокод", callback_data="use_promocode")
|
||||||
|
))
|
||||||
|
user_main_messages[user_id] = {
|
||||||
|
'chat_id': sent_message.chat.id,
|
||||||
|
'message_id': sent_message.message_id,
|
||||||
|
'state': None
|
||||||
|
}
|
||||||
|
|
||||||
@dp.message_handler(commands=['add_admin'])
|
@dp.message_handler(commands=['add_admin'])
|
||||||
async def add_admin_command(message: types.Message):
|
async def add_admin_command(message: types.Message):
|
||||||
@@ -177,11 +199,8 @@ 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):
|
||||||
user_id = message.from_user.id
|
user_id = message.from_user.id
|
||||||
if user_id not in admins and user_id not in moderators:
|
|
||||||
await message.answer("У вас нет доступа.")
|
|
||||||
return
|
|
||||||
|
|
||||||
user_state = user_main_messages.get(user_id, {}).get('state')
|
user_state = user_main_messages.get(user_id, {}).get('state')
|
||||||
|
|
||||||
if user_state == 'waiting_for_user_name':
|
if user_state == 'waiting_for_user_name':
|
||||||
user_name = message.text.strip()
|
user_name = message.text.strip()
|
||||||
if not re.match(r'^[a-zA-Z0-9_-]+$', user_name):
|
if not re.match(r'^[a-zA-Z0-9_-]+$', user_name):
|
||||||
@@ -194,10 +213,8 @@ async def handle_messages(message: types.Message):
|
|||||||
vpn_key = await generate_vpn_key(conf_path)
|
vpn_key = await generate_vpn_key(conf_path)
|
||||||
caption = f"Конфигурация для {user_name}:\nAmneziaVPN:\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```\n{vpn_key}\n```"
|
caption = f"Конфигурация для {user_name}:\nAmneziaVPN:\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```\n{vpn_key}\n```"
|
||||||
with open(conf_path, 'rb') as config:
|
with open(conf_path, 'rb') as config:
|
||||||
# Отправляем конфиг отдельным сообщением и закрепляем его
|
|
||||||
config_message = await bot.send_document(user_id, config, caption=caption, parse_mode="Markdown")
|
config_message = await bot.send_document(user_id, config, caption=caption, parse_mode="Markdown")
|
||||||
await bot.pin_chat_message(user_id, config_message.message_id, disable_notification=True)
|
await bot.pin_chat_message(user_id, config_message.message_id, disable_notification=True)
|
||||||
# Обновляем меню внизу, не закрепляя его
|
|
||||||
await bot.edit_message_text(
|
await bot.edit_message_text(
|
||||||
chat_id=user_main_messages[user_id]['chat_id'],
|
chat_id=user_main_messages[user_id]['chat_id'],
|
||||||
message_id=user_main_messages[user_id]['message_id'],
|
message_id=user_main_messages[user_id]['message_id'],
|
||||||
@@ -222,6 +239,41 @@ async def handle_messages(message: types.Message):
|
|||||||
user_main_messages[user_id]['state'] = None
|
user_main_messages[user_id]['state'] = None
|
||||||
except:
|
except:
|
||||||
await message.reply("Введите корректный Telegram ID.")
|
await message.reply("Введите корректный Telegram ID.")
|
||||||
|
elif user_state == 'waiting_for_promocode':
|
||||||
|
promocode = message.text.strip()
|
||||||
|
discount = db.apply_promocode(promocode)
|
||||||
|
if discount:
|
||||||
|
user_main_messages[user_id]['promocode_discount'] = discount
|
||||||
|
await message.reply(f"Промокод активирован! Скидка: {discount}%")
|
||||||
|
else:
|
||||||
|
await message.reply("Неверный или истёкший промокод.")
|
||||||
|
await bot.edit_message_text(
|
||||||
|
chat_id=user_main_messages[user_id]['chat_id'],
|
||||||
|
message_id=user_main_messages[user_id]['message_id'],
|
||||||
|
text="Выберите действие:",
|
||||||
|
reply_markup=get_main_menu_markup(user_id)
|
||||||
|
)
|
||||||
|
user_main_messages[user_id]['state'] = None
|
||||||
|
elif user_state == 'waiting_for_new_promocode' and user_id in admins:
|
||||||
|
try:
|
||||||
|
code, discount, days_valid, max_uses = message.text.strip().split()
|
||||||
|
discount = float(discount)
|
||||||
|
days_valid = int(days_valid)
|
||||||
|
max_uses = int(max_uses) if max_uses.lower() != 'none' 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):
|
||||||
|
await message.reply(f"Промокод {code} добавлен: скидка {discount}%, действует {days_valid} дней, макс. использований: {max_uses or 'неограничено'}")
|
||||||
|
else:
|
||||||
|
await message.reply("Промокод уже существует.")
|
||||||
|
except:
|
||||||
|
await message.reply("Формат: <код> <скидка%> <дней_действия> <макс_использований|none>")
|
||||||
|
await bot.edit_message_text(
|
||||||
|
chat_id=user_main_messages[user_id]['chat_id'],
|
||||||
|
message_id=user_main_messages[user_id]['message_id'],
|
||||||
|
text="Выберите действие:",
|
||||||
|
reply_markup=get_main_menu_markup(user_id)
|
||||||
|
)
|
||||||
|
user_main_messages[user_id]['state'] = None
|
||||||
|
|
||||||
@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):
|
||||||
@@ -261,70 +313,51 @@ async def client_selected_callback(callback_query: types.CallbackQuery):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info(f"Callback data: {callback_query.data}")
|
|
||||||
username = callback_query.data.split('client_')[1]
|
username = callback_query.data.split('client_')[1]
|
||||||
logger.info(f"Выбран клиент: {username}")
|
|
||||||
|
|
||||||
clients = db.get_client_list()
|
clients = db.get_client_list()
|
||||||
logger.info(f"Список клиентов: {clients}")
|
|
||||||
client_info = next((c for c in clients if c[0] == username), None)
|
client_info = next((c for c in clients if c[0] == username), None)
|
||||||
if not client_info:
|
if not client_info:
|
||||||
await callback_query.answer("Пользователь не найден.", show_alert=True)
|
await callback_query.answer("Пользователь не найден.", show_alert=True)
|
||||||
return
|
return
|
||||||
logger.info(f"Информация о клиенте: {client_info}")
|
|
||||||
|
|
||||||
# Базовые значения
|
|
||||||
status = "🔴 Офлайн"
|
status = "🔴 Офлайн"
|
||||||
incoming_traffic = "↓—"
|
incoming_traffic = "↓—"
|
||||||
outgoing_traffic = "↑—"
|
outgoing_traffic = "↑—"
|
||||||
ipv4_address = "—"
|
ipv4_address = "—"
|
||||||
|
expiration = db.get_user_expiration(username)
|
||||||
|
expiration_text = expiration.strftime("%Y-%m-%d %H:%M UTC") if expiration else "Не установлен"
|
||||||
|
|
||||||
if isinstance(client_info, (tuple, list)) and len(client_info) > 2 and client_info[2] is not None:
|
if isinstance(client_info, (tuple, list)) and len(client_info) > 2 and client_info[2] is not None:
|
||||||
logger.info(f"client_info[2]: {client_info[2]}")
|
|
||||||
ip_match = re.search(r'(\d{1,3}\.){3}\d{1,3}/\d+', str(client_info[2]))
|
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 "—"
|
ipv4_address = ip_match.group(0) if ip_match else "—"
|
||||||
else:
|
|
||||||
logger.info(f"client_info некорректно: {client_info}")
|
|
||||||
|
|
||||||
# Проверяем активность
|
|
||||||
active_clients = db.get_active_list()
|
active_clients = db.get_active_list()
|
||||||
logger.info(f"Список активных клиентов: {active_clients}")
|
|
||||||
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)
|
||||||
logger.info(f"Активная информация: {active_info}")
|
|
||||||
|
|
||||||
if (active_info is not None and isinstance(active_info, (tuple, list)) and
|
if active_info and isinstance(active_info, (tuple, list)) and len(active_info) > 2:
|
||||||
len(active_info) > 2 and active_info[1] is not None and
|
if active_info[1] and active_info[1].lower() not in ['never', 'нет данных', '-']:
|
||||||
active_info[1].lower() not in ['never', 'нет данных', '-']):
|
try:
|
||||||
logger.info(f"active_info[1]: {active_info[1]}")
|
last_handshake = parse_relative_time(active_info[1])
|
||||||
try:
|
|
||||||
last_handshake = parse_relative_time(active_info[1])
|
|
||||||
if last_handshake is not None:
|
|
||||||
logger.info(f"last_handshake: {last_handshake}")
|
|
||||||
status = "🟢 Онлайн" if (datetime.now(pytz.UTC) - last_handshake).total_seconds() <= 60 else "❌ Офлайн"
|
status = "🟢 Онлайн" if (datetime.now(pytz.UTC) - last_handshake).total_seconds() <= 60 else "❌ Офлайн"
|
||||||
if len(active_info) > 2 and active_info[2] is not None:
|
except:
|
||||||
logger.info(f"active_info[2]: {active_info[2]}")
|
pass
|
||||||
try:
|
if active_info[2]:
|
||||||
transfer_result = parse_transfer(active_info[2])
|
try:
|
||||||
if transfer_result is not None:
|
incoming_bytes, outgoing_bytes = parse_transfer(active_info[2])
|
||||||
incoming_bytes, outgoing_bytes = transfer_result
|
incoming_traffic = f"↓{humanize.naturalsize(incoming_bytes)}"
|
||||||
incoming_traffic = f"↓{humanize.naturalsize(incoming_bytes)}"
|
outgoing_traffic = f"↑{humanize.naturalsize(outgoing_bytes)}"
|
||||||
outgoing_traffic = f"↑{humanize.naturalsize(outgoing_bytes)}"
|
except:
|
||||||
else:
|
pass
|
||||||
logger.info("parse_transfer вернул None")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка в parse_transfer: {str(e)}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка в parse_relative_time: {str(e)}")
|
|
||||||
|
|
||||||
# Формируем текст профиля
|
|
||||||
text = (
|
text = (
|
||||||
f"📧 *Имя:* {username}\n"
|
f"📧 *Имя:* {username}\n"
|
||||||
f"🌐 *IPv4:* {ipv4_address}\n"
|
f"🌐 *IPv4:* {ipv4_address}\n"
|
||||||
f"🌐 *Статус:* {status}\n"
|
f"🌐 *Статус:* {status}\n"
|
||||||
f"🔼 *Исходящий:* {incoming_traffic}\n"
|
f"🔼 *Исходящий:* {incoming_traffic}\n"
|
||||||
f"🔽 *Входящий:* {outgoing_traffic}"
|
f"🔽 *Входящий:* {outgoing_traffic}\n"
|
||||||
|
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("ℹ️ IP info", callback_data=f"ip_info_{username}"),
|
||||||
InlineKeyboardButton("🔗 Подключения", callback_data=f"connections_{username}"),
|
InlineKeyboardButton("🔗 Подключения", callback_data=f"connections_{username}"),
|
||||||
@@ -333,7 +366,6 @@ async def client_selected_callback(callback_query: types.CallbackQuery):
|
|||||||
InlineKeyboardButton("🏠 Домой", callback_data="home")
|
InlineKeyboardButton("🏠 Домой", callback_data="home")
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Редактирование сообщения для {username}")
|
|
||||||
await bot.edit_message_text(
|
await bot.edit_message_text(
|
||||||
chat_id=callback_query.message.chat.id,
|
chat_id=callback_query.message.chat.id,
|
||||||
message_id=callback_query.message.message_id,
|
message_id=callback_query.message.message_id,
|
||||||
@@ -364,9 +396,7 @@ async def list_users_callback(callback_query: types.CallbackQuery):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info("Запуск list_users_callback")
|
|
||||||
clients = db.get_client_list()
|
clients = db.get_client_list()
|
||||||
logger.info(f"Клиенты: {clients}")
|
|
||||||
if not clients:
|
if not clients:
|
||||||
await bot.edit_message_text(
|
await bot.edit_message_text(
|
||||||
chat_id=callback_query.message.chat.id,
|
chat_id=callback_query.message.chat.id,
|
||||||
@@ -381,14 +411,9 @@ async def list_users_callback(callback_query: types.CallbackQuery):
|
|||||||
|
|
||||||
keyboard = InlineKeyboardMarkup(row_width=2)
|
keyboard = InlineKeyboardMarkup(row_width=2)
|
||||||
active_clients = {client[0]: client[1] for client in db.get_active_list()}
|
active_clients = {client[0]: client[1] for client in db.get_active_list()}
|
||||||
logger.info(f"Активные клиенты: {active_clients}")
|
|
||||||
now = datetime.now(pytz.UTC)
|
|
||||||
|
|
||||||
for client in clients:
|
for client in clients:
|
||||||
username = client[0]
|
username = client[0]
|
||||||
last_handshake = active_clients.get(username)
|
last_handshake = active_clients.get(username)
|
||||||
logger.info(f"Клиент: {username}, last_handshake: {last_handshake}")
|
|
||||||
# Упрощённая логика статуса для теста
|
|
||||||
status = "❌" if not last_handshake or last_handshake.lower() in ['never', 'нет данных', '-'] else "🟢"
|
status = "❌" if not last_handshake or last_handshake.lower() in ['never', 'нет данных', '-'] else "🟢"
|
||||||
button_text = f"{status} {username}"
|
button_text = f"{status} {username}"
|
||||||
keyboard.insert(InlineKeyboardButton(button_text, callback_data=f"client_{username}"))
|
keyboard.insert(InlineKeyboardButton(button_text, callback_data=f"client_{username}"))
|
||||||
@@ -519,6 +544,7 @@ async def client_delete_callback(callback_query: types.CallbackQuery):
|
|||||||
username = callback_query.data.split('delete_user_')[1]
|
username = callback_query.data.split('delete_user_')[1]
|
||||||
if db.deactive_user_db(username):
|
if db.deactive_user_db(username):
|
||||||
shutil.rmtree(os.path.join('users', username), ignore_errors=True)
|
shutil.rmtree(os.path.join('users', username), ignore_errors=True)
|
||||||
|
db.remove_user_expiration(username)
|
||||||
text = f"Пользователь **{username}** удален."
|
text = f"Пользователь **{username}** удален."
|
||||||
else:
|
else:
|
||||||
text = f"Не удалось удалить **{username}**."
|
text = f"Не удалось удалить **{username}**."
|
||||||
@@ -534,9 +560,6 @@ async def client_delete_callback(callback_query: types.CallbackQuery):
|
|||||||
@dp.callback_query_handler(lambda c: c.data == "home")
|
@dp.callback_query_handler(lambda c: c.data == "home")
|
||||||
async def return_home(callback_query: types.CallbackQuery):
|
async def return_home(callback_query: types.CallbackQuery):
|
||||||
user_id = callback_query.from_user.id
|
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
|
|
||||||
user_main_messages[user_id]['state'] = None
|
user_main_messages[user_id]['state'] = None
|
||||||
await bot.edit_message_text(
|
await bot.edit_message_text(
|
||||||
chat_id=callback_query.message.chat.id,
|
chat_id=callback_query.message.chat.id,
|
||||||
@@ -581,7 +604,6 @@ async def send_user_config(callback_query: types.CallbackQuery):
|
|||||||
vpn_key = await generate_vpn_key(conf_path)
|
vpn_key = await generate_vpn_key(conf_path)
|
||||||
caption = f"Конфигурация для {username}:\nAmneziaVPN:\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```\n{vpn_key}\n```"
|
caption = f"Конфигурация для {username}:\nAmneziaVPN:\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```\n{vpn_key}\n```"
|
||||||
with open(conf_path, 'rb') as config:
|
with open(conf_path, 'rb') as config:
|
||||||
# Отправляем конфиг отдельным сообщением и закрепляем его
|
|
||||||
config_message = await bot.send_document(user_id, config, caption=caption, parse_mode="Markdown")
|
config_message = await bot.send_document(user_id, config, caption=caption, parse_mode="Markdown")
|
||||||
await bot.pin_chat_message(user_id, config_message.message_id, disable_notification=True)
|
await bot.pin_chat_message(user_id, config_message.message_id, disable_notification=True)
|
||||||
else:
|
else:
|
||||||
@@ -686,10 +708,182 @@ async def pc_instructions(callback_query: types.CallbackQuery):
|
|||||||
)
|
)
|
||||||
await callback_query.answer()
|
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
|
||||||
|
keyboard = InlineKeyboardMarkup(row_width=2).add(
|
||||||
|
InlineKeyboardButton("1 месяц - $10", callback_data="select_period_1_month"),
|
||||||
|
InlineKeyboardButton("3 месяца - $25", callback_data="select_period_3_months"),
|
||||||
|
InlineKeyboardButton("6 месяцев - $45", callback_data="select_period_6_months"),
|
||||||
|
InlineKeyboardButton("🏠 Домой", callback_data="home")
|
||||||
|
)
|
||||||
|
await bot.edit_message_text(
|
||||||
|
chat_id=callback_query.message.chat.id,
|
||||||
|
message_id=callback_query.message.message_id,
|
||||||
|
text="Выберите период подписки:",
|
||||||
|
reply_markup=keyboard
|
||||||
|
)
|
||||||
|
await callback_query.answer()
|
||||||
|
|
||||||
|
@dp.callback_query_handler(lambda c: c.data.startswith('select_period_'))
|
||||||
|
async def select_period_callback(callback_query: types.CallbackQuery):
|
||||||
|
user_id = callback_query.from_user.id
|
||||||
|
period = callback_query.data.split('select_period_')[1]
|
||||||
|
price = PRICING[period]
|
||||||
|
discount = user_main_messages.get(user_id, {}).get('promocode_discount', 0)
|
||||||
|
final_price = price * (1 - discount / 100)
|
||||||
|
|
||||||
|
# Заглушка для создания платежа
|
||||||
|
payment_id = str(uuid.uuid4())
|
||||||
|
db.add_payment(user_id, payment_id, final_price, 'pending')
|
||||||
|
|
||||||
|
# Предполагается, что здесь будет интеграция с платежной системой
|
||||||
|
payment_url = f"https://example.com/pay/{payment_id}" # Замените на реальный URL платежной системы
|
||||||
|
|
||||||
|
keyboard = InlineKeyboardMarkup().add(
|
||||||
|
InlineKeyboardButton("💳 Оплатить", url=payment_url),
|
||||||
|
InlineKeyboardButton("⬅️ Назад", callback_data="buy_key"),
|
||||||
|
InlineKeyboardButton("🏠 Домой", callback_data="home")
|
||||||
|
)
|
||||||
|
await bot.edit_message_text(
|
||||||
|
chat_id=callback_query.message.chat.id,
|
||||||
|
message_id=callback_query.message.message_id,
|
||||||
|
text=f"Подписка на {period.replace('_', ' ')}: ${final_price:.2f} (скидка {discount}%)\nОплатите по ссылке:",
|
||||||
|
reply_markup=keyboard
|
||||||
|
)
|
||||||
|
user_main_messages[user_id]['pending_payment'] = {
|
||||||
|
'payment_id': payment_id,
|
||||||
|
'period': period,
|
||||||
|
'username': 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
|
||||||
|
await bot.edit_message_text(
|
||||||
|
chat_id=callback_query.message.chat.id,
|
||||||
|
message_id=callback_query.message.message_id,
|
||||||
|
text="Введите промокод:",
|
||||||
|
reply_markup=InlineKeyboardMarkup().add(InlineKeyboardButton("🏠 Домой", callback_data="home"))
|
||||||
|
)
|
||||||
|
user_main_messages[user_id]['state'] = 'waiting_for_promocode'
|
||||||
|
await callback_query.answer()
|
||||||
|
|
||||||
|
@dp.callback_query_handler(lambda c: c.data == "manage_promocodes")
|
||||||
|
async def manage_promocodes_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
|
||||||
|
promocodes = db.get_promocodes()
|
||||||
|
text = "Промокоды:\n" + "\n".join(
|
||||||
|
f"{code}: {info['discount']}% (использовано {info['uses']}/{info['max_uses'] or '∞'}, до {info['expires_at'] or 'неограничено'})"
|
||||||
|
for code, info in promocodes.items()
|
||||||
|
) if promocodes else "Промокоды отсутствуют."
|
||||||
|
keyboard = InlineKeyboardMarkup(row_width=2).add(
|
||||||
|
InlineKeyboardButton("➕ Добавить промокод", callback_data="add_promocode"),
|
||||||
|
InlineKeyboardButton("🗑️ Удалить промокод", callback_data="delete_promocode"),
|
||||||
|
InlineKeyboardButton("🏠 Домой", callback_data="home")
|
||||||
|
)
|
||||||
|
await bot.edit_message_text(
|
||||||
|
chat_id=callback_query.message.chat.id,
|
||||||
|
message_id=callback_query.message.message_id,
|
||||||
|
text=text,
|
||||||
|
reply_markup=keyboard
|
||||||
|
)
|
||||||
|
await callback_query.answer()
|
||||||
|
|
||||||
|
@dp.callback_query_handler(lambda c: c.data == "add_promocode")
|
||||||
|
async def add_promocode_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
|
||||||
|
await bot.edit_message_text(
|
||||||
|
chat_id=callback_query.message.chat.id,
|
||||||
|
message_id=callback_query.message.message_id,
|
||||||
|
text="Введите промокод в формате: <код> <скидка%> <дней_действия> <макс_использований|none>",
|
||||||
|
reply_markup=InlineKeyboardMarkup().add(InlineKeyboardButton("🏠 Домой", callback_data="home"))
|
||||||
|
)
|
||||||
|
user_main_messages[user_id]['state'] = 'waiting_for_new_promocode'
|
||||||
|
await callback_query.answer()
|
||||||
|
|
||||||
|
@dp.callback_query_handler(lambda c: c.data == "delete_promocode")
|
||||||
|
async def delete_promocode_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
|
||||||
|
promocodes = db.get_promocodes()
|
||||||
|
keyboard = InlineKeyboardMarkup(row_width=2)
|
||||||
|
for code in promocodes:
|
||||||
|
keyboard.insert(InlineKeyboardButton(f"🗑️ {code}", callback_data=f"remove_promocode_{code}"))
|
||||||
|
keyboard.add(InlineKeyboardButton("🏠 Домой", callback_data="home"))
|
||||||
|
await bot.edit_message_text(
|
||||||
|
chat_id=callback_query.message.chat.id,
|
||||||
|
message_id=callback_query.message.message_id,
|
||||||
|
text="Выберите промокод для удаления:",
|
||||||
|
reply_markup=keyboard
|
||||||
|
)
|
||||||
|
await callback_query.answer()
|
||||||
|
|
||||||
|
@dp.callback_query_handler(lambda c: c.data.startswith('remove_promocode_'))
|
||||||
|
async def remove_promocode_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
|
||||||
|
code = callback_query.data.split('remove_promocode_')[1]
|
||||||
|
if db.remove_promocode(code):
|
||||||
|
await callback_query.answer(f"Промокод {code} удалён.", show_alert=True)
|
||||||
|
else:
|
||||||
|
await callback_query.answer(f"Промокод {code} не найден.", show_alert=True)
|
||||||
|
await manage_promocodes_callback(callback_query)
|
||||||
|
|
||||||
|
async def check_expired_keys():
|
||||||
|
now = datetime.now(pytz.UTC)
|
||||||
|
for user, expiration, _ in db.get_users_with_expiration():
|
||||||
|
if expiration and datetime.fromisoformat(expiration).replace(tzinfo=pytz.UTC) < now:
|
||||||
|
db.deactive_user_db(user)
|
||||||
|
shutil.rmtree(os.path.join('users', user), ignore_errors=True)
|
||||||
|
db.remove_user_expiration(user)
|
||||||
|
logger.info(f"Пользователь {user} деактивирован из-за истечения срока действия.")
|
||||||
|
|
||||||
|
async def check_payment_status():
|
||||||
|
# Заглушка для проверки статуса платежа
|
||||||
|
# В реальной системе здесь будет запрос к API платежной системы
|
||||||
|
payments = db.get_all_payments()
|
||||||
|
for payment in payments:
|
||||||
|
if payment['status'] == 'pending':
|
||||||
|
# Предположим, что платеж подтвержден (заглушка)
|
||||||
|
db.update_payment_status(payment['payment_id'], 'completed')
|
||||||
|
user_id = payment['user_id']
|
||||||
|
if user_id in user_main_messages and 'pending_payment' in user_main_messages[user_id]:
|
||||||
|
pending = user_main_messages[user_id]['pending_payment']
|
||||||
|
period = pending['period']
|
||||||
|
months = {'1_month': 1, '3_months': 3, '6_months': 6}[period]
|
||||||
|
expiration = datetime.now(pytz.UTC) + timedelta(days=30 * months)
|
||||||
|
|
||||||
|
# Генерация уникального имени пользователя
|
||||||
|
username = f"user_{user_id}_{uuid.uuid4().hex[:8]}"
|
||||||
|
success = db.root_add(username, ipv6=False)
|
||||||
|
if success:
|
||||||
|
db.set_user_expiration(username, expiration, "Неограниченно")
|
||||||
|
conf_path = os.path.join('users', username, f'{username}.conf')
|
||||||
|
if os.path.exists(conf_path):
|
||||||
|
vpn_key = await generate_vpn_key(conf_path)
|
||||||
|
caption = f"Ваш ключ для {username} (действует до {expiration.strftime('%Y-%m-%d')}):\nAmneziaVPN:\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```\n{vpn_key}\n```"
|
||||||
|
with open(conf_path, 'rb') as config:
|
||||||
|
config_message = await bot.send_document(user_id, config, caption=caption, parse_mode="Markdown")
|
||||||
|
await bot.pin_chat_message(user_id, config_message.message_id, disable_notification=True)
|
||||||
|
user_main_messages[user_id].pop('pending_payment', None)
|
||||||
|
user_main_messages[user_id].pop('promocode_discount', None)
|
||||||
|
|
||||||
def parse_transfer(transfer_str):
|
def parse_transfer(transfer_str):
|
||||||
if not isinstance(transfer_str, str) or not transfer_str.strip():
|
if not isinstance(transfer_str, str) or not transfer_str.strip():
|
||||||
logger.error(f"Некорректный transfer_str: {transfer_str}")
|
logger.error(f"Некорректный transfer_str: {transfer_str}")
|
||||||
return 0, 0 # Значения по умолчанию
|
return 0, 0
|
||||||
try:
|
try:
|
||||||
incoming, outgoing = re.split(r'[/,]', transfer_str)[:2]
|
incoming, outgoing = re.split(r'[/,]', transfer_str)[:2]
|
||||||
size_map = {'B': 1, 'KB': 10**3, 'KiB': 1024, 'MB': 10**6, 'MiB': 1024**2, 'GB': 10**9, 'GiB': 1024**3}
|
size_map = {'B': 1, 'KB': 10**3, 'KiB': 1024, 'MB': 10**6, 'MiB': 1024**2, 'GB': 10**9, 'GiB': 1024**3}
|
||||||
@@ -706,7 +900,7 @@ def parse_transfer(transfer_str):
|
|||||||
return incoming_bytes, outgoing_bytes
|
return incoming_bytes, outgoing_bytes
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка в parse_transfer: {str(e)}")
|
logger.error(f"Ошибка в parse_transfer: {str(e)}")
|
||||||
return 0, 0 # Значения по умолчанию
|
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(
|
||||||
@@ -736,6 +930,8 @@ async def on_startup(dp):
|
|||||||
logger.error("Список админов пуст.")
|
logger.error("Список админов пуст.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
scheduler.add_job(db.ensure_peer_names, IntervalTrigger(minutes=1))
|
scheduler.add_job(db.ensure_peer_names, IntervalTrigger(minutes=1))
|
||||||
|
scheduler.add_job(check_expired_keys, IntervalTrigger(minutes=5))
|
||||||
|
scheduler.add_job(check_payment_status, IntervalTrigger(minutes=1))
|
||||||
|
|
||||||
async def on_shutdown(dp):
|
async def on_shutdown(dp):
|
||||||
scheduler.shutdown()
|
scheduler.shutdown()
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user