diff --git a/awg/bot_manager.py b/awg/bot_manager.py index b71f4a7..aebb470 100644 --- a/awg/bot_manager.py +++ b/awg/bot_manager.py @@ -13,6 +13,7 @@ import zipfile import ipaddress import humanize import shutil +import uuid from aiogram import Bot, types from aiogram.dispatcher import Dispatcher from aiogram.dispatcher.middlewares import BaseMiddleware @@ -45,6 +46,13 @@ WG_CONFIG_FILE = wg_config_file DOCKER_CONTAINER = docker_container ENDPOINT = endpoint +# Цены за периоды (в условных единицах) +PRICING = { + '1_month': 10.0, + '3_months': 25.0, + '6_months': 45.0 +} + class AdminMessageDeletionMiddleware(BaseMiddleware): async def on_process_message(self, message: types.Message, data: dict): if message.from_user.id in admins and message.text.startswith('/'): @@ -55,10 +63,9 @@ scheduler = AsyncIOScheduler(timezone=pytz.UTC) scheduler.start() dp.middleware.setup(AdminMessageDeletionMiddleware()) -# Главное меню с новым порядком и эмодзи +# Главное меню def get_main_menu_markup(user_id): markup = InlineKeyboardMarkup(row_width=2) - # Последовательное добавление кнопок с помощью add markup.add( InlineKeyboardButton("➕ Добавить пользователя", callback_data="add_user"), InlineKeyboardButton("📋 Список клиентов", callback_data="list_users") @@ -67,12 +74,19 @@ def get_main_menu_markup(user_id): InlineKeyboardButton("🔑 Получить конфиг", callback_data="get_config"), InlineKeyboardButton("ℹ️ Инструкция", callback_data="instructions") ) + markup.add( + InlineKeyboardButton("💳 Купить ключ", callback_data="buy_key"), + InlineKeyboardButton("🎟️ Использовать промокод", callback_data="use_promocode") + ) if user_id in admins: markup.add( InlineKeyboardButton("👥 Список админов", callback_data="list_admins"), 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 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: if not isinstance(relative_str, str) or not relative_str.strip(): logger.error(f"Некорректный relative_str: {relative_str}") - return datetime.now(pytz.UTC) # Значение по умолчанию + return datetime.now(pytz.UTC) try: relative_str = relative_str.lower().replace(' ago', '') delta = 0 @@ -144,7 +158,7 @@ def parse_relative_time(relative_str: str) -> datetime: 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) # Значение по умолчанию + return datetime.now(pytz.UTC) @dp.message_handler(commands=['start', 'help']) 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] = { 'chat_id': sent_message.chat.id, 'message_id': sent_message.message_id, - 'state': None # Инициализируем state явно + 'state': None } 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']) async def add_admin_command(message: types.Message): @@ -177,11 +199,8 @@ async def add_admin_command(message: types.Message): @dp.message_handler() async def handle_messages(message: types.Message): 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') + if user_state == 'waiting_for_user_name': user_name = message.text.strip() 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) 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: - # Отправляем конфиг отдельным сообщением и закрепляем его 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.edit_message_text( chat_id=user_main_messages[user_id]['chat_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 except: 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") async def prompt_for_user_name(callback_query: types.CallbackQuery): @@ -261,70 +313,51 @@ async def client_selected_callback(callback_query: types.CallbackQuery): return try: - logger.info(f"Callback data: {callback_query.data}") username = callback_query.data.split('client_')[1] - logger.info(f"Выбран клиент: {username}") - clients = db.get_client_list() - logger.info(f"Список клиентов: {clients}") client_info = next((c for c in clients if c[0] == username), None) if not client_info: await callback_query.answer("Пользователь не найден.", show_alert=True) return - logger.info(f"Информация о клиенте: {client_info}") - # Базовые значения 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 "Не установлен" + 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])) ipv4_address = ip_match.group(0) if ip_match else "—" - else: - logger.info(f"client_info некорректно: {client_info}") - # Проверяем активность 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) - logger.info(f"Активная информация: {active_info}") - if (active_info is not None and isinstance(active_info, (tuple, list)) and - len(active_info) > 2 and active_info[1] is not None and - active_info[1].lower() not in ['never', 'нет данных', '-']): - logger.info(f"active_info[1]: {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}") + 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: + last_handshake = parse_relative_time(active_info[1]) status = "🟢 Онлайн" if (datetime.now(pytz.UTC) - last_handshake).total_seconds() <= 60 else "❌ Офлайн" - if len(active_info) > 2 and active_info[2] is not None: - logger.info(f"active_info[2]: {active_info[2]}") - try: - transfer_result = parse_transfer(active_info[2]) - if transfer_result is not None: - incoming_bytes, outgoing_bytes = transfer_result - incoming_traffic = f"↓{humanize.naturalsize(incoming_bytes)}" - outgoing_traffic = f"↑{humanize.naturalsize(outgoing_bytes)}" - else: - 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)}") + 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: + pass - # Формируем текст профиля text = ( f"📧 *Имя:* {username}\n" f"🌐 *IPv4:* {ipv4_address}\n" f"🌐 *Статус:* {status}\n" f"🔼 *Исходящий:* {incoming_traffic}\n" - f"🔽 *Входящий:* {outgoing_traffic}" + 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}"), @@ -333,7 +366,6 @@ async def client_selected_callback(callback_query: types.CallbackQuery): InlineKeyboardButton("🏠 Домой", callback_data="home") ) - logger.info(f"Редактирование сообщения для {username}") await bot.edit_message_text( chat_id=callback_query.message.chat.id, message_id=callback_query.message.message_id, @@ -364,9 +396,7 @@ async def list_users_callback(callback_query: types.CallbackQuery): return try: - logger.info("Запуск list_users_callback") clients = db.get_client_list() - logger.info(f"Клиенты: {clients}") if not clients: await bot.edit_message_text( 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) 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: username = client[0] 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 "🟢" button_text = f"{status} {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] if db.deactive_user_db(username): shutil.rmtree(os.path.join('users', username), ignore_errors=True) + db.remove_user_expiration(username) text = f"Пользователь **{username}** удален." else: 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") async def return_home(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 user_main_messages[user_id]['state'] = None await bot.edit_message_text( 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) 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: - # Отправляем конфиг отдельным сообщением и закрепляем его 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) else: @@ -686,10 +708,182 @@ async def pc_instructions(callback_query: types.CallbackQuery): ) 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): if not isinstance(transfer_str, str) or not transfer_str.strip(): logger.error(f"Некорректный transfer_str: {transfer_str}") - return 0, 0 # Значения по умолчанию + return 0, 0 try: 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} @@ -706,7 +900,7 @@ def parse_transfer(transfer_str): return incoming_bytes, outgoing_bytes except Exception as e: logger.error(f"Ошибка в parse_transfer: {str(e)}") - return 0, 0 # Значения по умолчанию + return 0, 0 async def generate_vpn_key(conf_path: str) -> str: process = await asyncio.create_subprocess_exec( @@ -736,6 +930,8 @@ async def on_startup(dp): logger.error("Список админов пуст.") sys.exit(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): scheduler.shutdown()