From 3d9865ce4c15883dd53fdda739e39ba64abfae46 Mon Sep 17 00:00:00 2001 From: stevefoxru <53931856+stevefoxru@users.noreply.github.com> Date: Sat, 1 Mar 2025 18:38:32 +0300 Subject: [PATCH] Update bot_manager.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлен функционал: 1. Администраторы и модераторы. 2. Инструкции по использованию VPN. --- awg/bot_manager.py | 1468 +++++++++++++------------------------------- 1 file changed, 432 insertions(+), 1036 deletions(-) diff --git a/awg/bot_manager.py b/awg/bot_manager.py index d448c85..4bb2362 100644 --- a/awg/bot_manager.py +++ b/awg/bot_manager.py @@ -5,7 +5,6 @@ import asyncio import aiofiles import os import re -import tempfile import json import subprocess import sys @@ -19,9 +18,8 @@ from aiogram.dispatcher import Dispatcher from aiogram.dispatcher.middlewares import BaseMiddleware from aiogram.utils import executor from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton -from datetime import datetime, timedelta +from datetime import datetime from apscheduler.schedulers.asyncio import AsyncIOScheduler -from apscheduler.triggers.date import DateTrigger from apscheduler.triggers.interval import IntervalTrigger logging.basicConfig(level=logging.INFO) @@ -30,18 +28,18 @@ logger = logging.getLogger(__name__) # Загрузка конфигурации setting = db.get_config() bot_token = setting.get('bot_token') -admin_ids = setting.get('admin_ids', []) # Ожидаем список ID +admin_ids = setting.get('admin_ids', []) +moderator_ids = setting.get('moderator_ids', []) wg_config_file = setting.get('wg_config_file') docker_container = setting.get('docker_container') endpoint = setting.get('endpoint') if not all([bot_token, admin_ids, wg_config_file, docker_container, endpoint]): - logger.error("Некоторые обязательные настройки отсутствуют в конфигурационном файле.") + logger.error("Некоторые обязательные настройки отсутствуют.") sys.exit(1) -# Преобразуем admin_ids в список целых чисел admins = [int(admin_id) for admin_id in admin_ids] - +moderators = [int(mod_id) for mod_id in moderator_ids] bot = Bot(bot_token) WG_CONFIG_FILE = wg_config_file DOCKER_CONTAINER = docker_container @@ -49,30 +47,32 @@ ENDPOINT = endpoint class AdminMessageDeletionMiddleware(BaseMiddleware): async def on_process_message(self, message: types.Message, data: dict): - if message.from_user.id in admins: - asyncio.create_task(delete_message_after_delay(message.chat.id, message.message_id, delay=2)) + if message.from_user.id in admins and message.text.startswith('/'): + asyncio.create_task(delete_message_after_delay(message.chat.id, message.message_id)) dp = Dispatcher(bot) scheduler = AsyncIOScheduler(timezone=pytz.UTC) scheduler.start() - dp.middleware.setup(AdminMessageDeletionMiddleware()) -main_menu_markup = InlineKeyboardMarkup(row_width=1).add( - InlineKeyboardButton("Добавить пользователя", callback_data="add_user"), - InlineKeyboardButton("Получить конфигурацию пользователя", callback_data="get_config"), - InlineKeyboardButton("Список клиентов", callback_data="list_users"), - InlineKeyboardButton("Создать бекап", callback_data="create_backup"), - InlineKeyboardButton("Список админов", callback_data="list_admins"), - InlineKeyboardButton("Добавить администратора", callback_data="add_admin") -) +# Главное меню с новым порядком и эмодзи +def get_main_menu_markup(user_id): + markup = InlineKeyboardMarkup(row_width=2) + markup.add(InlineKeyboardButton("➕ Добавить пользователя", callback_data="add_user")) + markup.insert(InlineKeyboardButton("📋 Список клиентов", callback_data="list_users")) + markup.insert(InlineKeyboardButton("🔑 Получить конфиг", callback_data="get_config")) + if user_id in admins: + markup.insert(InlineKeyboardButton("👥 Список админов", callback_data="list_admins")) + markup.insert(InlineKeyboardButton("👤 Добавить админа", callback_data="add_admin")) + if user_id in admins: + markup.add(InlineKeyboardButton("💾 Создать бекап", callback_data="create_backup")) + markup.add(InlineKeyboardButton("ℹ️ Инструкция", callback_data="instructions")) + return markup user_main_messages = {} isp_cache = {} ISP_CACHE_FILE = 'files/isp_cache.json' -CACHE_TTL = timedelta(hours=24) - -TRAFFIC_LIMITS = ["5 GB", "10 GB", "30 GB", "100 GB", "Неограниченно"] +CACHE_TTL = 24 * 3600 # 24 часа в секундах def get_interface_name(): return os.path.basename(WG_CONFIG_FILE).split('.')[0] @@ -81,84 +81,34 @@ 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: - try: - isp_cache = json.loads(await f.read()) - for ip in list(isp_cache.keys()): - isp_cache[ip]['timestamp'] = datetime.fromisoformat(isp_cache[ip]['timestamp']) - except: - isp_cache = {} + isp_cache = json.loads(await f.read()) async def save_isp_cache(): async with aiofiles.open(ISP_CACHE_FILE, 'w') as f: - cache_to_save = {ip: {'isp': data['isp'], 'timestamp': data['timestamp'].isoformat()} for ip, data in isp_cache.items()} - await f.write(json.dumps(cache_to_save)) + await f.write(json.dumps(isp_cache)) async def get_isp_info(ip: str) -> str: - now = datetime.now(pytz.UTC) - if ip in isp_cache and now - isp_cache[ip]['timestamp'] < CACHE_TTL: + 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: - ip_obj = ipaddress.ip_address(ip) - if ip_obj.is_private: + if ipaddress.ip_address(ip).is_private: return "Private Range" except: return "Invalid IP" - url = f"http://ip-api.com/json/{ip}?fields=status,message,isp" - try: - async with aiohttp.ClientSession() as session: - async with session.get(url) as resp: - if resp.status == 200: - data = await resp.json() - if data.get('status') == 'success': - isp = data.get('isp', 'Unknown ISP') - isp_cache[ip] = {'isp': isp, 'timestamp': now} - await save_isp_cache() - return isp - except: - pass + + 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 cleanup_isp_cache(): - now = datetime.now(pytz.UTC) - for ip in list(isp_cache.keys()): - if now - isp_cache[ip]['timestamp'] >= CACHE_TTL: - del isp_cache[ip] - await save_isp_cache() - -async def cleanup_connection_data(username: str): - file_path = os.path.join('files', 'connections', f'{username}_ip.json') - if os.path.exists(file_path): - async with aiofiles.open(file_path, 'r') as f: - try: - data = json.loads(await f.read()) - except: - data = {} - sorted_ips = sorted(data.items(), key=lambda x: datetime.strptime(x[1], '%d.%m.%Y %H:%M'), reverse=True) - limited_ips = dict(sorted_ips[:100]) - async with aiofiles.open(file_path, 'w') as f: - await f.write(json.dumps(limited_ips)) - -async def load_isp_cache_task(): - await load_isp_cache() - scheduler.add_job(cleanup_isp_cache, 'interval', hours=1) - -def create_zip(backup_filepath): - with zipfile.ZipFile(backup_filepath, 'w') as zipf: - for main_file in ['awg-decode.py', 'newclient.sh', 'removeclient.sh']: - if os.path.exists(main_file): - zipf.write(main_file, main_file) - for root, dirs, files in os.walk('files'): - for file in files: - filepath = os.path.join(root, file) - arcname = os.path.relpath(filepath, os.getcwd()) - zipf.write(filepath, arcname) - for root, dirs, files in os.walk('users'): - for file in files: - filepath = os.path.join(root, file) - arcname = os.path.relpath(filepath, os.getcwd()) - zipf.write(filepath, arcname) - -async def delete_message_after_delay(chat_id: int, message_id: int, delay: int): +async def delete_message_after_delay(chat_id: int, message_id: int, delay: int = 2): await asyncio.sleep(delay) try: await bot.delete_message(chat_id, message_id) @@ -166,1071 +116,517 @@ async def delete_message_after_delay(chat_id: int, message_id: int, delay: int): pass def parse_relative_time(relative_str: str) -> datetime: - try: - parts = relative_str.lower().replace(' ago', '').split(', ') - delta = timedelta() - for part in parts: - number, unit = part.split(' ') - number = int(number) - if 'minute' in unit: - delta += timedelta(minutes=number) - elif 'second' in unit: - delta += timedelta(seconds=number) - elif 'hour' in unit: - delta += timedelta(hours=number) - elif 'day' in unit: - delta += timedelta(days=number) - elif 'week' in unit: - delta += timedelta(weeks=number) - elif 'month' in unit: - delta += timedelta(days=30 * number) - elif 'year' in unit: - delta += timedelta(days=365 * number) - return datetime.now(pytz.UTC) - delta - except Exception as e: - logger.error(f"Ошибка при парсинге относительного времени '{relative_str}': {e}") - return None + 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) @dp.message_handler(commands=['start', 'help']) async def help_command_handler(message: types.Message): - if message.chat.id in admins: - sent_message = await message.answer("Выберите действие:", reply_markup=main_menu_markup) - user_main_messages[message.from_user.id] = {'chat_id': sent_message.chat.id, 'message_id': sent_message.message_id} - try: - await bot.pin_chat_message(chat_id=message.chat.id, message_id=sent_message.message_id, disable_notification=True) - except: - pass + user_id = message.from_user.id + if user_id in admins or user_id in moderators: + 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} + # Не закрепляем меню else: await message.answer("У вас нет доступа к этому боту.") @dp.message_handler(commands=['add_admin']) async def add_admin_command(message: types.Message): if message.from_user.id not in admins: - await message.answer("У вас нет прав для выполнения этого действия.") + await message.answer("У вас нет прав.") return try: new_admin_id = int(message.text.split()[1]) - if new_admin_id in admins: - await message.answer(f"Пользователь {new_admin_id} уже является администратором.") - return - db.add_admin(new_admin_id) - admins.append(new_admin_id) - await message.answer(f"Пользователь {new_admin_id} добавлен в администраторы.") - logger.info(f"Админ {message.from_user.id} добавил администратора {new_admin_id}") - await bot.send_message(new_admin_id, "Вы были назначены администратором бота!") - except (IndexError, ValueError): - await message.answer("Используйте формат: /add_admin ") - except Exception as e: - logger.error(f"Ошибка при добавлении администратора: {e}") - await message.answer("Произошла ошибка при добавлении администратора.") - -@dp.message_handler(commands=['remove_admin']) -async def remove_admin_command(message: types.Message): - if message.from_user.id not in admins: - await message.answer("У вас нет прав для выполнения этого действия.") - return - try: - admin_id_to_remove = int(message.text.split()[1]) - if admin_id_to_remove not in admins: - await message.answer(f"Пользователь {admin_id_to_remove} не является администратором.") - return - if len(admins) <= 1: - await message.answer("Нельзя удалить последнего администратора!") - return - db.remove_admin(admin_id_to_remove) - admins.remove(admin_id_to_remove) - await message.answer(f"Пользователь {admin_id_to_remove} удалён из администраторов.") - logger.info(f"Админ {message.from_user.id} удалил администратора {admin_id_to_remove}") - await bot.send_message(admin_id_to_remove, "Вы были удалены из администраторов бота.") - except (IndexError, ValueError): - await message.answer("Используйте формат: /remove_admin ") - except Exception as e: - logger.error(f"Ошибка при удалении администратора: {e}") - await message.answer("Произошла ошибка при удалении администратора.") + if new_admin_id not in admins: + db.add_admin(new_admin_id) + admins.append(new_admin_id) + await message.answer(f"Админ {new_admin_id} добавлен.") + await bot.send_message(new_admin_id, "Вы назначены администратором!") + except: + await message.answer("Формат: /add_admin ") @dp.message_handler() async def handle_messages(message: types.Message): - if message.chat.id not in admins: - await message.answer("У вас нет доступа к этому боту.") + 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(message.from_user.id, {}).get('state') + + user_state = user_main_messages.get(user_id, {}).get('state') if user_state == 'waiting_for_user_name': user_name = message.text.strip() - if not all(c.isalnum() or c in "-_" for c in user_name): - await message.reply("Имя пользователя может содержать только буквы, цифры, дефисы и подчёркивания.") - asyncio.create_task(delete_message_after_delay(message.chat.id, message.message_id, delay=2)) + if not re.match(r'^[a-zA-Z0-9_-]+$', user_name): + await message.reply("Имя может содержать только буквы, цифры, - и _.") return - user_main_messages[message.from_user.id]['client_name'] = user_name - user_main_messages[message.from_user.id]['state'] = 'waiting_for_duration' - duration_buttons = [ - InlineKeyboardButton("1 час", callback_data=f"duration_1h_{user_name}_noipv6"), - InlineKeyboardButton("1 день", callback_data=f"duration_1d_{user_name}_noipv6"), - InlineKeyboardButton("1 неделя", callback_data=f"duration_1w_{user_name}_noipv6"), - InlineKeyboardButton("1 месяц", callback_data=f"duration_1m_{user_name}_noipv6"), - InlineKeyboardButton("Без ограничений", callback_data=f"duration_unlimited_{user_name}_noipv6"), - InlineKeyboardButton("Домой", callback_data="home") - ] - duration_markup = InlineKeyboardMarkup(row_width=1).add(*duration_buttons) - main_chat_id = user_main_messages[message.from_user.id].get('chat_id') - main_message_id = user_main_messages[message.from_user.id].get('message_id') - if main_chat_id and main_message_id: - await bot.edit_message_text( - chat_id=main_chat_id, - message_id=main_message_id, - text=f"Выберите время действия конфигурации для пользователя **{user_name}**:", - parse_mode="Markdown", - reply_markup=duration_markup - ) - else: - await message.answer("Ошибка: главное сообщение не найдено.") - elif user_state == 'waiting_for_admin_id': + success = db.root_add(user_name, ipv6=False) + if success: + conf_path = os.path.join('users', user_name, f'{user_name}.conf') + if os.path.exists(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```" + 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'], + text="Выберите действие:", + reply_markup=get_main_menu_markup(user_id) + ) + user_main_messages[user_id]['state'] = None + elif user_state == 'waiting_for_admin_id' and user_id in admins: try: new_admin_id = int(message.text.strip()) - if new_admin_id in admins: - await message.reply(f"Пользователь {new_admin_id} уже является администратором.") - asyncio.create_task(delete_message_after_delay(message.chat.id, message.message_id, delay=2)) - return - db.add_admin(new_admin_id) - admins.append(new_admin_id) - await message.reply(f"Пользователь {new_admin_id} добавлен в администраторы.") - await bot.send_message(new_admin_id, "Вы были назначены администратором бота!") - logger.info(f"Админ {message.from_user.id} добавил администратора {new_admin_id} через интерфейс") - - # Возвращаем главное меню - main_chat_id = user_main_messages[message.from_user.id].get('chat_id') - main_message_id = user_main_messages[message.from_user.id].get('message_id') - if main_chat_id and main_message_id: - user_main_messages[message.from_user.id]['state'] = None - await bot.edit_message_text( - chat_id=main_chat_id, - message_id=main_message_id, - text="Выберите действие:", - reply_markup=main_menu_markup - ) - except ValueError: - await message.reply("Пожалуйста, введите корректный Telegram ID (число).") - asyncio.create_task(delete_message_after_delay(message.chat.id, message.message_id, delay=2)) - return - except Exception as e: - logger.error(f"Ошибка при добавлении администратора: {e}") - await message.reply("Произошла ошибка при добавлении администратора.") - asyncio.create_task(delete_message_after_delay(message.chat.id, message.message_id, delay=2)) - else: - await message.reply("Неизвестная команда или действие.") - asyncio.create_task(delete_message_after_delay(message.chat.id, message.message_id, delay=2)) + if new_admin_id not in admins: + db.add_admin(new_admin_id) + admins.append(new_admin_id) + await message.reply(f"Админ {new_admin_id} добавлен.") + await bot.send_message(new_admin_id, "Вы назначены администратором!") + 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 + except: + await message.reply("Введите корректный Telegram ID.") + +@dp.callback_query_handler(lambda c: c.data == "add_user") +async def prompt_for_user_name(callback_query: types.CallbackQuery): + user_id = callback_query.from_user.id + if user_id not in admins and user_id not in moderators: + 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="Введите имя пользователя:", + reply_markup=InlineKeyboardMarkup().add(InlineKeyboardButton("🏠 Домой", callback_data="home")) + ) + user_main_messages[user_id]['state'] = 'waiting_for_user_name' + await callback_query.answer() @dp.callback_query_handler(lambda c: c.data == "add_admin") async def prompt_for_admin_id(callback_query: types.CallbackQuery): - if callback_query.from_user.id not in admins: - await callback_query.answer("У вас нет прав для выполнения этого действия.", show_alert=True) + user_id = callback_query.from_user.id + if user_id not in admins: + await callback_query.answer("Нет прав.", show_alert=True) return - - main_chat_id = user_main_messages.get(callback_query.from_user.id, {}).get('chat_id') - main_message_id = user_main_messages.get(callback_query.from_user.id, {}).get('message_id') - if main_chat_id and main_message_id: - await bot.edit_message_text( - chat_id=main_chat_id, - message_id=main_message_id, - text="Введите Telegram ID нового администратора:", - reply_markup=InlineKeyboardMarkup().add( - InlineKeyboardButton("Домой", callback_data="home") - ) - ) - user_main_messages[callback_query.from_user.id]['state'] = 'waiting_for_admin_id' - else: - await callback_query.answer("Ошибка: главное сообщение не найдено.", show_alert=True) - await callback_query.answer() - -@dp.callback_query_handler(lambda c: c.data.startswith('add_user')) -async def prompt_for_user_name(callback_query: types.CallbackQuery): - if callback_query.from_user.id not in admins: - await callback_query.answer("У вас нет прав для выполнения этого действия.", show_alert=True) - return - main_chat_id = user_main_messages.get(callback_query.from_user.id, {}).get('chat_id') - main_message_id = user_main_messages.get(callback_query.from_user.id, {}).get('message_id') - if main_chat_id and main_message_id: - await bot.edit_message_text( - chat_id=main_chat_id, - message_id=main_message_id, - text="Введите имя пользователя для добавления:", - reply_markup=InlineKeyboardMarkup().add( - InlineKeyboardButton("Домой", callback_data="home") - ) - ) - user_main_messages[callback_query.from_user.id]['state'] = 'waiting_for_user_name' - else: - await callback_query.answer("Ошибка: главное сообщение не найдено.", show_alert=True) - await callback_query.answer() - -def parse_traffic_limit(traffic_limit: str) -> int: - mapping = {'B':1, 'KB':10**3, 'MB':10**6, 'GB':10**9, 'TB':10**12} - match = re.match(r'^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB)$', traffic_limit, re.IGNORECASE) - if match: - value = float(match.group(1)) - unit = match.group(2).upper() - return int(value * mapping.get(unit, 1)) - else: - return None - -@dp.callback_query_handler(lambda c: c.data.startswith('duration_')) -async def set_config_duration(callback: types.CallbackQuery): - if callback_query.from_user.id not in admins: - await callback_query.answer("У вас нет прав для выполнения этого действия.", show_alert=True) - return - parts = callback.data.split('_') - if len(parts) < 4: - await callback_query.answer("Некорректные данные.", show_alert=True) - return - duration_choice = parts[1] - client_name = parts[2] - ipv6_flag = parts[3] - user_main_messages[callback_query.from_user.id]['duration_choice'] = duration_choice - user_main_messages[callback_query.from_user.id]['state'] = 'waiting_for_traffic_limit' - traffic_buttons = [ - InlineKeyboardButton(limit, callback_data=f"traffic_limit_{limit}_{client_name}") - for limit in TRAFFIC_LIMITS - ] - traffic_markup = InlineKeyboardMarkup(row_width=1).add(*traffic_buttons) await bot.edit_message_text( - chat_id=callback.message.chat.id, - message_id=callback.message.message_id, - text=f"Выберите лимит трафика для пользователя **{client_name}**:", - parse_mode="Markdown", - reply_markup=traffic_markup + chat_id=callback_query.message.chat.id, + message_id=callback_query.message.message_id, + text="Введите Telegram ID нового админа:", + reply_markup=InlineKeyboardMarkup().add(InlineKeyboardButton("🏠 Домой", callback_data="home")) ) - await callback_query.answer() - -def format_vpn_key(vpn_key, num_lines=8): - line_length = len(vpn_key) // num_lines - if len(vpn_key) % num_lines != 0: - line_length += 1 - lines = [vpn_key[i:i+line_length] for i in range(0, len(vpn_key), line_length)] - return '\n'.join(lines) - -@dp.callback_query_handler(lambda c: c.data.startswith('traffic_limit_')) -async def set_traffic_limit(callback_query: types.CallbackQuery): - if callback_query.from_user.id not in admins: - await callback_query.answer("У вас нет прав для выполнения этого действия.", show_alert=True) - return - parts = callback_query.data.split('_', 3) - if len(parts) < 4: - await callback_query.answer("Некорректные данные.", show_alert=True) - return - traffic_limit = parts[2] - client_name = parts[3] - traffic_bytes = parse_traffic_limit(traffic_limit) - if traffic_limit != "Неограниченно" and traffic_bytes is None: - await callback_query.answer("Некорректный формат лимита трафика.", show_alert=True) - return - user_main_messages[callback_query.from_user.id]['traffic_limit'] = traffic_limit - user_main_messages[callback_query.from_user.id]['state'] = None - duration_choice = user_main_messages.get(callback_query.from_user.id, {}).get('duration_choice') - if duration_choice == '1h': - duration = timedelta(hours=1) - elif duration_choice == '1d': - duration = timedelta(days=1) - elif duration_choice == '1w': - duration = timedelta(weeks=1) - elif duration_choice == '1m': - duration = timedelta(days=30) - elif duration_choice == 'unlimited': - duration = None - else: - duration = None - if duration: - expiration_time = datetime.now(pytz.UTC) + duration - db.set_user_expiration(client_name, expiration_time, traffic_limit) - scheduler.add_job( - deactivate_user, - trigger=DateTrigger(run_date=expiration_time), - args=[client_name], - id=client_name - ) - confirmation_text = f"Пользователь **{client_name}** добавлен. \nКонфигурация истечет через **{duration_choice}**." - else: - db.set_user_expiration(client_name, None, traffic_limit) - confirmation_text = f"Пользователь **{client_name}** добавлен с неограниченным временем действия." - if traffic_limit != "Неограниченно": - confirmation_text += f"\nЛимит трафика: **{traffic_limit}**." - else: - confirmation_text += f"\nЛимит трафика: **♾️ Неограниченно**." - success = db.root_add(client_name, ipv6=False) - if success: - try: - conf_path = os.path.join('users', client_name, f'{client_name}.conf') - vpn_key = "" - if os.path.exists(conf_path): - vpn_key = await generate_vpn_key(conf_path) - if vpn_key: - instruction_text = ( - "\nAmneziaVPN [Google Play](https://play.google.com/store/apps/details?id=org.amnezia.vpn&hl=ru), " - "[GitHub](https://github.com/amnezia-vpn/amnezia-client)" - ) - formatted_key = format_vpn_key(vpn_key) - key_message = f"```\n{formatted_key}\n```" - caption = f"{instruction_text}\n{key_message}" - else: - caption = "VPN ключ не был сгенерирован." - if os.path.exists(conf_path): - with open(conf_path, 'rb') as config: - sent_doc = await bot.send_document( - callback_query.from_user.id, - config, - caption=caption, - parse_mode="Markdown", - disable_notification=True - ) - asyncio.create_task(delete_message_after_delay(callback_query.from_user.id, sent_doc.message_id, delay=15)) - except FileNotFoundError: - confirmation_text = "Не удалось найти файлы конфигурации для указанного пользователя." - sent_message = await bot.send_message(callback_query.from_user.id, confirmation_text, parse_mode="Markdown", disable_notification=True) - asyncio.create_task(delete_message_after_delay(callback_query.from_user.id, sent_message.message_id, delay=15)) - await callback_query.answer() - return - except Exception as e: - logger.error(f"Ошибка при отправке конфигурации: {e}") - confirmation_text = "Произошла ошибка." - sent_message = await bot.send_message(callback_query.from_user.id, confirmation_text, parse_mode="Markdown", disable_notification=True) - asyncio.create_task(delete_message_after_delay(callback_query.from_user.id, sent_message.message_id, delay=15)) - await callback_query.answer() - return - sent_confirmation = await bot.send_message( - chat_id=callback_query.from_user.id, - text=confirmation_text, - parse_mode="Markdown", - disable_notification=True - ) - asyncio.create_task(delete_message_after_delay(callback_query.from_user.id, sent_confirmation.message_id, delay=15)) - else: - confirmation_text = "Не удалось добавить пользователя." - sent_confirmation = await bot.send_message( - chat_id=callback_query.from_user.id, - text=confirmation_text, - parse_mode="Markdown", - disable_notification=True - ) - asyncio.create_task(delete_message_after_delay(callback_query.from_user.id, sent_confirmation.message_id, delay=15)) - main_chat_id = user_main_messages.get(callback_query.from_user.id, {}).get('chat_id') - main_message_id = user_main_messages.get(callback_query.from_user.id, {}).get('message_id') - if main_chat_id and main_message_id: - await bot.edit_message_text( - chat_id=main_chat_id, - message_id=main_message_id, - text="Выберите действие:", - reply_markup=main_menu_markup - ) - else: - await callback_query.answer("Выберите действие:", show_alert=True) + user_main_messages[user_id]['state'] = 'waiting_for_admin_id' await callback_query.answer() @dp.callback_query_handler(lambda c: c.data.startswith('client_')) async def client_selected_callback(callback_query: types.CallbackQuery): - if callback_query.from_user.id not in admins: - await callback_query.answer("У вас нет прав для выполнения этого действия.", show_alert=True) + 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('client_', 1) - username = username.strip() + username = callback_query.data.split('client_')[1] clients = db.get_client_list() client_info = next((c for c in clients if c[0] == username), None) if not client_info: - await callback_query.answer("Ошибка: пользователь не найден.", show_alert=True) + await callback_query.answer("Пользователь не найден.", show_alert=True) return - expiration_time = db.get_user_expiration(username) - traffic_limit = db.get_user_traffic_limit(username) + status = "🔴 Офлайн" incoming_traffic = "↓—" outgoing_traffic = "↑—" - ipv4_address = "—" - total_bytes = 0 - formatted_total = "0.00B" + ipv4_address = re.search(r'(\d{1,3}\.){3}\d{1,3}/\d+', client_info[2]) or "—" active_clients = db.get_active_list() active_info = next((ac for ac in active_clients if ac[0] == username), None) - if active_info: - last_handshake_str = active_info[1] - if last_handshake_str.lower() not in ['never', 'нет данных', '-']: - try: - last_handshake_dt = parse_relative_time(last_handshake_str) - if last_handshake_dt: - delta = datetime.now(pytz.UTC) - last_handshake_dt - if delta <= timedelta(minutes=1): - status = "🟢 Онлайн" - else: - status = "❌ Офлайн" - transfer = active_info[2] - incoming_bytes, outgoing_bytes = parse_transfer(transfer) - incoming_traffic = f"↓{humanize_bytes(incoming_bytes)}" - outgoing_traffic = f"↑{humanize_bytes(outgoing_bytes)}" - traffic_data = await update_traffic(username, incoming_bytes, outgoing_bytes) - total_bytes = traffic_data.get('total_incoming', 0) + traffic_data.get('total_outgoing', 0) - formatted_total = humanize_bytes(total_bytes) - if traffic_limit != "Неограниченно": - limit_bytes = parse_traffic_limit(traffic_limit) - if total_bytes >= limit_bytes: - await deactivate_user(username) - await callback_query.answer(f"Пользователь **{username}** превысил лимит трафика и был удален.", show_alert=True) - return - except ValueError: - logger.error(f"Некорректный формат даты для пользователя {username}: {last_handshake_str}") - status = "❌ Офлайн" - else: - traffic_data = await read_traffic(username) - total_bytes = traffic_data.get('total_incoming', 0) + traffic_data.get('total_outgoing', 0) - formatted_total = humanize_bytes(total_bytes) - allowed_ips = client_info[2] - ipv4_match = re.search(r'(\d{1,3}\.){3}\d{1,3}/\d+', allowed_ips) - if ipv4_match: - ipv4_address = ipv4_match.group(0) - else: - ipv4_address = "—" - if expiration_time: - now = datetime.now(pytz.UTC) - try: - expiration_dt = expiration_time - if expiration_dt.tzinfo is None: - expiration_dt = expiration_dt.replace(tzinfo=pytz.UTC) - remaining = expiration_dt - now - if remaining.total_seconds() > 0: - days, seconds = remaining.days, remaining.seconds - hours = seconds // 3600 - minutes = (seconds % 3600) // 60 - date_end = f"📅 {days}д {hours}ч {minutes}м" - else: - date_end = "📅 ♾️ Неограниченно" - except Exception as e: - logger.error(f"Ошибка при обработке даты окончания: {e}") - date_end = "📅 ♾️ Неограниченно" - else: - date_end = "📅 ♾️ Неограниченно" - if traffic_limit == "Неограниченно": - traffic_limit_display = "♾️ Неограниченно" - else: - traffic_limit_display = traffic_limit + + if active_info and active_info[1].lower() not in ['never', 'нет данных', '-']: + last_handshake = parse_relative_time(active_info[1]) + status = "🟢 Онлайн" if (datetime.now(pytz.UTC) - last_handshake).total_seconds() <= 60 else "❌ Офлайн" + incoming_bytes, outgoing_bytes = parse_transfer(active_info[2]) + incoming_traffic = f"↓{humanize.naturalsize(incoming_bytes)}" + outgoing_traffic = f"↑{humanize.naturalsize(outgoing_bytes)}" + text = ( f"📧 *Имя:* {username}\n" f"🌐 *IPv4:* {ipv4_address}\n" - f"🌐 *Статус соединения:* {status}\n" - f"{date_end}\n" - f"🔼 *Исходящий трафик:* {incoming_traffic}\n" - f"🔽 *Входящий трафик:* {outgoing_traffic}\n" - f"📊 *Всего:* ↑↓{formatted_total} из **{traffic_limit_display}**\n" + f"🌐 *Статус:* {status}\n" + f"🔼 *Исходящий:* {incoming_traffic}\n" + f"🔽 *Входящий:* {outgoing_traffic}" ) - keyboard = InlineKeyboardMarkup(row_width=2) - keyboard.add( - InlineKeyboardButton("IP info", callback_data=f"ip_info_{username}"), - InlineKeyboardButton("Подключения", callback_data=f"connections_{username}") + 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="list_users"), + InlineKeyboardButton("🏠 Домой", callback_data="home") ) - keyboard.add( - InlineKeyboardButton("Удалить", callback_data=f"delete_user_{username}") - ) - keyboard.add( - InlineKeyboardButton("Назад", callback_data="list_users"), - InlineKeyboardButton("Домой", callback_data="home") - ) - main_chat_id = user_main_messages.get(callback_query.from_user.id, {}).get('chat_id') - main_message_id = user_main_messages.get(callback_query.from_user.id, {}).get('message_id') - if main_chat_id and main_message_id: - try: - await bot.edit_message_text( - chat_id=main_chat_id, - message_id=main_message_id, - text=text, - parse_mode="Markdown", - reply_markup=keyboard - ) - except Exception as e: - logger.error(f"Ошибка при редактировании сообщения: {e}") - await callback_query.answer("Ошибка при обновлении сообщения.", show_alert=True) - else: - await callback_query.answer("Ошибка: главное сообщение не найдено.", show_alert=True) - await callback_query.answer() - -@dp.callback_query_handler(lambda c: c.data.startswith('list_users')) -async def list_users_callback(callback_query: types.CallbackQuery): - if callback_query.from_user.id not in admins: - await callback_query.answer("У вас нет прав для выполнения этого действия.", show_alert=True) - return - clients = db.get_client_list() - if not clients: - await callback_query.answer("Список пользователей пуст.", show_alert=True) - return - active_clients = db.get_active_list() - active_clients_dict = {client[0]: client[1] for client in active_clients} - keyboard = InlineKeyboardMarkup(row_width=2) - now = datetime.now(pytz.UTC) - for client in clients: - username = client[0] - last_handshake_str = active_clients_dict.get(username) - if last_handshake_str and last_handshake_str.lower() not in ['never', 'нет данных', '-']: - try: - last_handshake_dt = parse_relative_time(last_handshake_str) - if last_handshake_dt: - delta = now - last_handshake_dt - delta_days = delta.days - if delta_days <= 5: - status_display = f"🟢({delta_days}d) {username}" - else: - status_display = f"❌(?d) {username}" - else: - status_display = f"❌(?d) {username}" - except ValueError: - logger.error(f"Некорректный формат даты для пользователя {username}: {last_handshake_str}") - status_display = f"❌(?d) {username}" - else: - status_display = f"❌(?d) {username}" - keyboard.insert(InlineKeyboardButton(status_display, callback_data=f"client_{username}")) - keyboard.add(InlineKeyboardButton("Домой", callback_data="home")) - main_chat_id = user_main_messages.get(callback_query.from_user.id, {}).get('chat_id') - main_message_id = user_main_messages.get(callback_query.from_user.id, {}).get('message_id') - if main_chat_id and main_message_id: - try: - await bot.edit_message_text( - chat_id=main_chat_id, - message_id=main_message_id, - text="Выберите пользователя:", - reply_markup=keyboard - ) - except Exception as e: - logger.error(f"Ошибка при редактировании сообщения: {e}") - await callback_query.answer("Ошибка при обновлении сообщения.", show_alert=True) - else: - sent_message = await callback_query.message.reply("Выберите пользователя:", reply_markup=keyboard) - user_main_messages[callback_query.from_user.id] = {'chat_id': sent_message.chat.id, 'message_id': sent_message.message_id} - try: - await bot.pin_chat_message(chat_id=sent_message.chat.id, message_id=sent_message.message_id, disable_notification=True) - except: - pass - await callback_query.answer() - -@dp.callback_query_handler(lambda c: c.data.startswith('list_admins')) -async def list_admins_callback(callback_query: types.CallbackQuery): - if callback_query.from_user.id not in admins: - await callback_query.answer("У вас нет прав для выполнения этого действия.", show_alert=True) - return - admin_list = "\n".join([f"- {admin_id}" for admin_id in admins]) - text = f"Список администраторов:\n{admin_list}" - keyboard = InlineKeyboardMarkup(row_width=2) - for admin_id in admins: - keyboard.insert(InlineKeyboardButton(f"Удалить {admin_id}", callback_data=f"remove_admin_{admin_id}")) - 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=text, + parse_mode="Markdown", reply_markup=keyboard ) await callback_query.answer() +@dp.callback_query_handler(lambda c: c.data == "list_users") +async def list_users_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 + clients = db.get_client_list() + if not clients: + await callback_query.answer("Список пуст.", show_alert=True) + return + + keyboard = InlineKeyboardMarkup(row_width=2) + active_clients = {client[0]: client[1] for client in db.get_active_list()} + now = datetime.now(pytz.UTC) + for client in clients: + username = client[0] + last_handshake = active_clients.get(username) + status = "❌" if not last_handshake or last_handshake.lower() in ['never', 'нет данных', '-'] else "🟢" if (now - parse_relative_time(last_handshake)).days <= 5 else "❌" + keyboard.insert(InlineKeyboardButton(f"{status} {username}", callback_data=f"client_{username}")) + 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 == "list_admins") +async def list_admins_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 + keyboard = InlineKeyboardMarkup(row_width=2) + for admin_id in admins: + keyboard.insert(InlineKeyboardButton(f"🗑️ Удалить {admin_id}", callback_data=f"remove_admin_{admin_id}")) + 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=f"Администраторы:\n" + "\n".join(f"- {admin_id}" for admin_id in admins), + reply_markup=keyboard + ) + await callback_query.answer() + +@dp.callback_query_handler(lambda c: c.data.startswith('remove_admin_')) +async def remove_admin_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 + admin_id = int(callback_query.data.split('_')[2]) + if admin_id not in admins or len(admins) <= 1: + await callback_query.answer("Нельзя удалить последнего админа или несуществующего.", show_alert=True) + return + db.remove_admin(admin_id) + admins.remove(admin_id) + await bot.send_message(admin_id, "Вы удалены из администраторов.") + await list_admins_callback(callback_query) + @dp.callback_query_handler(lambda c: c.data.startswith('connections_')) async def client_connections_callback(callback_query: types.CallbackQuery): - if callback_query.from_user.id not in admins: - await callback_query.answer("У вас нет прав для выполнения этого действия.", show_alert=True) + 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) - username = username.strip() + 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) + await callback_query.answer("Нет данных о подключениях.", show_alert=True) return - try: - async with aiofiles.open(file_path, 'r') as f: - data = json.loads(await f.read()) - sorted_ips = sorted(data.items(), key=lambda x: datetime.strptime(x[1], '%d.%m.%Y %H:%M'), reverse=True) - last_connections = sorted_ips[:5] - isp_tasks = [get_isp_info(ip) for ip, _ in last_connections] - isp_results = await asyncio.gather(*isp_tasks) - connections_text = f"*Последние подключения пользователя {username}:*\n" - for (ip, timestamp), isp in zip(last_connections, isp_results): - connections_text += f"{ip} ({isp}) - {timestamp}\n" - keyboard = InlineKeyboardMarkup(row_width=2) - keyboard.add( - InlineKeyboardButton("Назад", callback_data=f"client_{username}"), - InlineKeyboardButton("Домой", callback_data="home") - ) - await bot.edit_message_text( - chat_id=callback_query.message.chat.id, - message_id=callback_query.message.message_id, - text=connections_text, - parse_mode="Markdown", - reply_markup=keyboard - ) - except Exception as e: - logger.error(f"Ошибка при получении данных о подключениях для пользователя {username}: {e}") - await callback_query.answer("Ошибка при получении данных о подключениях.", show_alert=True) - return - await cleanup_connection_data(username) + + 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") + ) + await bot.edit_message_text( + chat_id=callback_query.message.chat.id, + message_id=callback_query.message.message_id, + text=text, + parse_mode="Markdown", + reply_markup=keyboard + ) await callback_query.answer() @dp.callback_query_handler(lambda c: c.data.startswith('ip_info_')) async def ip_info_callback(callback_query: types.CallbackQuery): - if callback_query.from_user.id not in admins: - await callback_query.answer("У вас нет прав для выполнения этого действия.", show_alert=True) + 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) - username = username.strip() - active_clients = db.get_active_list() - active_info = next((ac for ac in active_clients if ac[0] == username), None) - if active_info: - endpoint = active_info[3] - ip_address = endpoint.split(':')[0] - else: - await callback_query.answer("Нет информации о подключении пользователя.", show_alert=True) + 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 - url = f"http://ip-api.com/json/{ip_address}?fields=message,country,countryCode,region,regionName,city,zip,lat,lon,timezone,isp,org,as,hosting" - try: - async with aiohttp.ClientSession() as session: - async with session.get(url) as resp: - if resp.status == 200: - data = await resp.json() - if 'message' in data: - await callback_query.answer(f"Ошибка при получении данных: {data['message']}", show_alert=True) - return - else: - await callback_query.answer(f"Ошибка при запросе к API: {resp.status}", show_alert=True) - return - except Exception as e: - logger.error(f"Ошибка при запросе к API: {e}") - await callback_query.answer("Ошибка при запросе к API.", show_alert=True) - return - info_text = f"*IP информация для {username}:*\n" - for key, value in data.items(): - info_text += f"{key.capitalize()}: {value}\n" - keyboard = InlineKeyboardMarkup(row_width=2) - keyboard.add( - InlineKeyboardButton("Назад", callback_data=f"client_{username}"), - InlineKeyboardButton("Домой", callback_data="home") + + 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") + ) + await bot.edit_message_text( + chat_id=callback_query.message.chat.id, + message_id=callback_query.message.message_id, + text=text, + parse_mode="Markdown", + reply_markup=keyboard ) - main_chat_id = user_main_messages.get(callback_query.from_user.id, {}).get('chat_id') - main_message_id = user_main_messages.get(callback_query.from_user.id, {}).get('message_id') - if main_chat_id and main_message_id: - try: - await bot.edit_message_text( - chat_id=main_chat_id, - message_id=main_message_id, - text=info_text, - parse_mode="Markdown", - reply_markup=keyboard - ) - except Exception as e: - logger.error(f"Ошибка при изменении сообщения: {e}") - await callback_query.answer("Ошибка при обновлении сообщения.", show_alert=True) - return - else: - await callback_query.answer("Ошибка: главное сообщение не найдено.", show_alert=True) - return await callback_query.answer() @dp.callback_query_handler(lambda c: c.data.startswith('delete_user_')) async def client_delete_callback(callback_query: types.CallbackQuery): - if callback_query.from_user.id not in admins: - await callback_query.answer("У вас нет прав для выполнения этого действия.", show_alert=True) + 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('delete_user_')[1] - success = db.deactive_user_db(username) - if success: - db.remove_user_expiration(username) - try: - scheduler.remove_job(job_id=username) - except: - pass - user_dir = os.path.join('users', username) - try: - if os.path.exists(user_dir): - shutil.rmtree(user_dir) - except Exception as e: - logger.error(f"Ошибка при удалении директории для пользователя {username}: {e}") - confirmation_text = f"Пользователь **{username}** успешно удален." + if db.deactive_user_db(username): + shutil.rmtree(os.path.join('users', username), ignore_errors=True) + text = f"Пользователь **{username}** удален." else: - confirmation_text = f"Не удалось удалить пользователя **{username}**." - main_chat_id = user_main_messages.get(callback_query.from_user.id, {}).get('chat_id') - main_message_id = user_main_messages.get(callback_query.from_user.id, {}).get('message_id') - if main_chat_id and main_message_id: - await bot.edit_message_text( - chat_id=main_chat_id, - message_id=main_message_id, - text=confirmation_text, - parse_mode="Markdown", - reply_markup=main_menu_markup - ) - else: - await callback_query.answer("Ошибка: главное сообщение не найдено.", show_alert=True) - return + text = f"Не удалось удалить **{username}**." + await bot.edit_message_text( + chat_id=callback_query.message.chat.id, + message_id=callback_query.message.message_id, + text=text, + parse_mode="Markdown", + reply_markup=get_main_menu_markup(user_id) + ) await callback_query.answer() -@dp.callback_query_handler(lambda c: c.data.startswith('home')) +@dp.callback_query_handler(lambda c: c.data == "home") async def return_home(callback_query: types.CallbackQuery): - if callback_query.from_user.id not in admins: - await callback_query.answer("У вас нет прав для выполнения этого действия.", show_alert=True) + 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 - main_chat_id = user_main_messages.get(callback_query.from_user.id, {}).get('chat_id') - main_message_id = user_main_messages.get(callback_query.from_user.id, {}).get('message_id') - if main_chat_id and main_message_id: - user_main_messages[callback_query.from_user.id].pop('state', None) - user_main_messages[callback_query.from_user.id].pop('client_name', None) - user_main_messages[callback_query.from_user.id].pop('duration_choice', None) - user_main_messages[callback_query.from_user.id].pop('traffic_limit', None) - try: - await bot.edit_message_text( - chat_id=main_chat_id, - message_id=main_message_id, - text="Выберите действие:", - reply_markup=main_menu_markup - ) - except: - sent_message = await callback_query.message.reply("Выберите действие:", reply_markup=main_menu_markup) - user_main_messages[callback_query.from_user.id] = {'chat_id': sent_message.chat.id, 'message_id': sent_message.message_id} - try: - await bot.pin_chat_message(chat_id=sent_message.chat.id, message_id=sent_message.message_id, disable_notification=True) - except: - pass - else: - sent_message = await callback_query.message.reply("Выберите действие:", reply_markup=main_menu_markup) - user_main_messages[callback_query.from_user.id] = {'chat_id': sent_message.chat.id, 'message_id': sent_message.message_id} - try: - await bot.pin_chat_message(chat_id=sent_message.chat.id, message_id=sent_message.message_id, disable_notification=True) - except: - pass + user_main_messages[user_id]['state'] = None + await bot.edit_message_text( + chat_id=callback_query.message.chat.id, + message_id=callback_query.message.message_id, + text="Выберите действие:", + reply_markup=get_main_menu_markup(user_id) + ) await callback_query.answer() -@dp.callback_query_handler(lambda c: c.data.startswith('get_config')) +@dp.callback_query_handler(lambda c: c.data == "get_config") async def list_users_for_config(callback_query: types.CallbackQuery): - if callback_query.from_user.id not in admins: - await callback_query.answer("У вас нет прав для выполнения этого действия.", show_alert=True) + 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 clients = db.get_client_list() if not clients: - await callback_query.answer("Список пользователей пуст.", show_alert=True) + await callback_query.answer("Список пуст.", show_alert=True) return + keyboard = InlineKeyboardMarkup(row_width=2) for client in clients: - username = client[0] - keyboard.insert(InlineKeyboardButton(username, callback_data=f"send_config_{username}")) - keyboard.add(InlineKeyboardButton("Домой", callback_data="home")) - main_chat_id = user_main_messages.get(callback_query.from_user.id, {}).get('chat_id') - main_message_id = user_main_messages.get(callback_query.from_user.id, {}).get('message_id') - if main_chat_id and main_message_id: - await bot.edit_message_text( - chat_id=main_chat_id, - message_id=main_message_id, - text="Выберите пользователя для получения конфигурации:", - reply_markup=keyboard - ) - else: - sent_message = await callback_query.message.reply("Выберите пользователя для получения конфигурации:", reply_markup=keyboard) - user_main_messages[callback_query.from_user.id] = {'chat_id': sent_message.chat.id, 'message_id': sent_message.message_id} - try: - await bot.pin_chat_message(chat_id=sent_message.chat.id, message_id=sent_message.message_id, disable_notification=True) - except: - pass + keyboard.insert(InlineKeyboardButton(client[0], callback_data=f"send_config_{client[0]}")) + 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('send_config_')) async def send_user_config(callback_query: types.CallbackQuery): - if callback_query.from_user.id not in admins: - await callback_query.answer("У вас нет прав для выполнения этого действия.", show_alert=True) - return - _, username = callback_query.data.split('send_config_', 1) - username = username.strip() - sent_messages = [] - try: - user_dir = os.path.join('users', username) - conf_path = os.path.join(user_dir, f'{username}.conf') - if not os.path.exists(conf_path): - await callback_query.answer("Конфигурационный файл пользователя отсутствует.", show_alert=True) - return - if os.path.exists(conf_path): - vpn_key = await generate_vpn_key(conf_path) - if vpn_key: - instruction_text = ( - "\nAmneziaVPN [Google Play](https://play.google.com/store/apps/details?id=org.amnezia.vpn&hl=ru), " - "[GitHub](https://github.com/amnezia-vpn/amnezia-client)" - ) - formatted_key = format_vpn_key(vpn_key) - key_message = f"```\n{formatted_key}\n```" - caption = f"{instruction_text}\n{key_message}" - else: - caption = "VPN ключ не был сгенерирован." - with open(conf_path, 'rb') as config: - sent_doc = await bot.send_document( - callback_query.from_user.id, - config, - caption=caption, - parse_mode="Markdown", - disable_notification=True - ) - sent_messages.append(sent_doc.message_id) - else: - confirmation_text = f"Не удалось создать конфигурацию для пользователя **{username}**." - sent_message = await bot.send_message(callback_query.from_user.id, confirmation_text, parse_mode="Markdown", disable_notification=True) - asyncio.create_task(delete_message_after_delay(callback_query.from_user.id, sent_message.message_id, delay=15)) - await callback_query.answer() - return - except Exception as e: - confirmation_text = f"Произошла ошибка: {e}" - sent_message = await bot.send_message(callback_query.from_user.id, confirmation_text, parse_mode="Markdown", disable_notification=True) - asyncio.create_task(delete_message_after_delay(callback_query.from_user.id, sent_message.message_id, delay=15)) - await callback_query.answer() - return - if not sent_messages: - confirmation_text = f"Не удалось найти файлы конфигурации для пользователя **{username}**." - sent_message = await bot.send_message(callback_query.from_user.id, confirmation_text, parse_mode="Markdown", disable_notification=True) - asyncio.create_task(delete_message_after_delay(callback_query.from_user.id, sent_message.message_id, delay=15)) - await callback_query.answer() + 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('send_config_')[1] + 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}:\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: - confirmation_text = f"Конфигурация для **{username}** отправлена." - sent_confirmation = await bot.send_message( - chat_id=callback_query.from_user.id, - text=confirmation_text, - parse_mode="Markdown", - disable_notification=True - ) - asyncio.create_task(delete_message_after_delay(callback_query.from_user.id, sent_confirmation.message_id, delay=15)) - for message_id in sent_messages: - asyncio.create_task(delete_message_after_delay(callback_query.from_user.id, message_id, delay=15)) + await bot.send_message(user_id, f"Конфигурация для **{username}** не найдена.", parse_mode="Markdown") await callback_query.answer() -@dp.callback_query_handler(lambda c: c.data.startswith('create_backup')) +@dp.callback_query_handler(lambda c: c.data == "create_backup") async def create_backup_callback(callback_query: types.CallbackQuery): - if callback_query.from_user.id not in admins: - await callback_query.answer("У вас нет прав для выполнения этого действия.", show_alert=True) + user_id = callback_query.from_user.id + if user_id not in admins: + await callback_query.answer("Нет прав.", show_alert=True) return - date_str = datetime.now().strftime('%Y-%m-%d') - backup_filename = f"backup_{date_str}.zip" - backup_filepath = os.path.join(os.getcwd(), backup_filename) - try: - loop = asyncio.get_running_loop() - await loop.run_in_executor(None, create_zip, backup_filepath) - if os.path.exists(backup_filepath): - with open(backup_filepath, 'rb') as f: - await bot.send_document(callback_query.from_user.id, f, caption=backup_filename, disable_notification=True) - os.remove(backup_filepath) - else: - logger.error(f"Бекап файл не создан: {backup_filepath}") - await bot.send_message(callback_query.from_user.id, "Не удалось создать бекап.", disable_notification=True) - except Exception as e: - logger.error(f"Ошибка при создании бекапа: {e}") - await bot.send_message(callback_query.from_user.id, "Не удалось создать бекап.", disable_notification=True) + backup_filename = f"backup_{datetime.now().strftime('%Y-%m-%d')}.zip" + with zipfile.ZipFile(backup_filename, 'w') as zipf: + for file in ['awg-decode.py', 'newclient.sh', 'removeclient.sh']: + if os.path.exists(file): + zipf.write(file) + for root, _, files in os.walk('files'): + for file in files: + zipf.write(os.path.join(root, file), os.path.relpath(os.path.join(root, file), os.getcwd())) + for root, _, files in os.walk('users'): + for file in files: + zipf.write(os.path.join(root, file), os.path.relpath(os.path.join(root, file), os.getcwd())) + with open(backup_filename, 'rb') as f: + await bot.send_document(user_id, f, caption=backup_filename) + os.remove(backup_filename) + 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") + ) + 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 == "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") + ) + await bot.edit_message_text( + chat_id=callback_query.message.chat.id, + message_id=callback_query.message.message_id, + text=instruction_text, + parse_mode="Markdown", + reply_markup=keyboard + ) + 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") + ) + await bot.edit_message_text( + chat_id=callback_query.message.chat.id, + message_id=callback_query.message.message_id, + text=instruction_text, + parse_mode="Markdown", + reply_markup=keyboard + ) await callback_query.answer() def parse_transfer(transfer_str): - try: - if '/' in transfer_str: - incoming, outgoing = transfer_str.split('/') - incoming = incoming.strip() - outgoing = outgoing.strip() - incoming_match = re.match(r'([\d.]+)\s*(\w+)', incoming) - outgoing_match = re.match(r'([\d.]+)\s*(\w+)', outgoing) - def convert_to_bytes(value, unit): - size_map = { - 'B': 1, 'KB': 10**3, 'KiB': 1024, 'MB': 10**6, 'MiB': 1024**2, 'GB': 10**9, 'GiB': 1024**3, - } - return float(value) * size_map.get(unit, 1) - incoming_bytes = convert_to_bytes(*incoming_match.groups()) if incoming_match else 0 - outgoing_bytes = convert_to_bytes(*outgoing_match.groups()) if outgoing_match else 0 - return incoming_bytes, outgoing_bytes - else: - parts = re.split(r'[/,]', transfer_str) - if len(parts) >= 2: - incoming = parts[0].strip() - outgoing = parts[1].strip() - incoming_match = re.match(r'([\d.]+)\s*(\w+)', incoming) - outgoing_match = re.match(r'([\d.]+)\s*(\w+)', outgoing) - def convert_to_bytes(value, unit): - size_map = { - 'B': 1, 'KB': 10**3, 'KiB': 1024, 'MB': 10**6, 'MiB': 1024**2, 'GB': 10**9, 'GiB': 1024**3, - } - return float(value) * size_map.get(unit, 1) - incoming_bytes = convert_to_bytes(*incoming_match.groups()) if incoming_match else 0 - outgoing_bytes = convert_to_bytes(*outgoing_match.groups()) if outgoing_match else 0 - return incoming_bytes, outgoing_bytes - else: - return 0, 0 - except Exception as e: - logger.error(f"Ошибка при парсинге трафика: {e}") - return 0, 0 - -def humanize_bytes(bytes_value): - return humanize.naturalsize(bytes_value, binary=False) - -async def read_traffic(username): - traffic_file = os.path.join('users', username, 'traffic.json') - os.makedirs(os.path.dirname(traffic_file), exist_ok=True) - if not os.path.exists(traffic_file): - traffic_data = { - "total_incoming": 0, - "total_outgoing": 0, - "last_incoming": 0, - "last_outgoing": 0 - } - async with aiofiles.open(traffic_file, 'w') as f: - await f.write(json.dumps(traffic_data)) - return traffic_data - else: - async with aiofiles.open(traffic_file, 'r') as f: - content = await f.read() - try: - traffic_data = json.loads(content) - return traffic_data - except json.JSONDecodeError: - logger.error(f"Ошибка при чтении traffic.json для пользователя {username}. Инициализация заново.") - traffic_data = { - "total_incoming": 0, - "total_outgoing": 0, - "last_incoming": 0, - "last_outgoing": 0 - } - async with aiofiles.open(traffic_file, 'w') as f_write: - await f_write.write(json.dumps(traffic_data)) - return traffic_data - -async def update_traffic(username, incoming_bytes, outgoing_bytes): - traffic_data = await read_traffic(username) - delta_incoming = incoming_bytes - traffic_data.get('last_incoming', 0) - delta_outgoing = outgoing_bytes - traffic_data.get('last_outgoing', 0) - if delta_incoming < 0: - delta_incoming = 0 - if delta_outgoing < 0: - delta_outgoing = 0 - traffic_data['total_incoming'] += delta_incoming - traffic_data['total_outgoing'] += delta_outgoing - traffic_data['last_incoming'] = incoming_bytes - traffic_data['last_outgoing'] = outgoing_bytes - traffic_file = os.path.join('users', username, 'traffic.json') - async with aiofiles.open(traffic_file, 'w') as f: - await f.write(json.dumps(traffic_data)) - return traffic_data - -async def update_all_clients_traffic(): - logger.info("Начало обновления трафика для всех клиентов.") - active_clients = db.get_active_list() - for client in active_clients: - username = client[0] - transfer = client[2] - incoming_bytes, outgoing_bytes = parse_transfer(transfer) - traffic_data = await update_traffic(username, incoming_bytes, outgoing_bytes) - logger.info(f"Обновлён трафик для пользователя {username}: Входящий {traffic_data['total_incoming']} B, Исходящий {traffic_data['total_outgoing']} B") - traffic_limit = db.get_user_traffic_limit(username) - if traffic_limit != "Неограниченно": - limit_bytes = parse_traffic_limit(traffic_limit) - total_bytes = traffic_data.get('total_incoming', 0) + traffic_data.get('total_outgoing', 0) - if total_bytes >= limit_bytes: - await deactivate_user(username) - logger.info("Завершено обновление трафика для всех клиентов.") + 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} + for unit, multiplier in size_map.items(): + if unit in incoming: + incoming_bytes = float(re.match(r'([\d.]+)', incoming)[0]) * multiplier + if unit in outgoing: + outgoing_bytes = float(re.match(r'([\d.]+)', outgoing)[0]) * multiplier + return incoming_bytes, outgoing_bytes async def generate_vpn_key(conf_path: str) -> str: - try: - process = await asyncio.create_subprocess_exec( - 'python3.11', - 'awg-decode.py', - '--encode', - conf_path, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - stdout, stderr = await process.communicate() - if process.returncode != 0: - logger.error(f"awg-decode.py ошибка: {stderr.decode().strip()}") - return "" - vpn_key = stdout.decode().strip() - if vpn_key.startswith('vpn://'): - return vpn_key - else: - logger.error(f"awg-decode.py вернул некорректный формат: {vpn_key}") - return "" - except Exception as e: - logger.error(f"Ошибка при вызове awg-decode.py: {e}") - return "" - -async def deactivate_user(client_name: str): - success = db.deactive_user_db(client_name) - if success: - db.remove_user_expiration(client_name) - try: - scheduler.remove_job(job_id=client_name) - except: - pass - user_dir = os.path.join('users', client_name) - try: - if os.path.exists(user_dir): - shutil.rmtree(user_dir) - except Exception as e: - logger.error(f"Ошибка при удалении директории для пользователя {client_name}: {e}") - confirmation_text = f"Конфигурация пользователя **{client_name}** была деактивирована из-за превышения лимита трафика." - for admin_id in admins: - sent_message = await bot.send_message(admin_id, confirmation_text, parse_mode="Markdown", disable_notification=True) - asyncio.create_task(delete_message_after_delay(admin_id, sent_message.message_id, delay=15)) - else: - for admin_id in admins: - sent_message = await bot.send_message(admin_id, f"Не удалось деактивировать пользователя **{client_name}**.", parse_mode="Markdown", disable_notification=True) - asyncio.create_task(delete_message_after_delay(admin_id, sent_message.message_id, delay=15)) + process = await asyncio.create_subprocess_exec( + 'python3.11', 'awg-decode.py', '--encode', conf_path, + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await process.communicate() + return stdout.decode().strip() if process.returncode == 0 and stdout.decode().startswith('vpn://') else "" async def check_environment(): - try: - cmd = f"docker ps --filter 'name={DOCKER_CONTAINER}' --format '{{{{.Names}}}}'" - container_names = subprocess.check_output(cmd, shell=True).decode().strip().split('\n') - if DOCKER_CONTAINER not in container_names: - logger.error(f"Контейнер Docker '{DOCKER_CONTAINER}' не найден.") - return False - except subprocess.CalledProcessError as e: - logger.error(f"Ошибка при проверке Docker-контейнера: {e}") - return False - try: - cmd = f"docker exec {DOCKER_CONTAINER} test -f {WG_CONFIG_FILE}" - subprocess.check_call(cmd, shell=True) - except subprocess.CalledProcessError: - logger.error(f"Конфигурационный файл WireGuard '{WG_CONFIG_FILE}' не найден в контейнере '{DOCKER_CONTAINER}'.") + if DOCKER_CONTAINER not in subprocess.check_output(f"docker ps --filter 'name={DOCKER_CONTAINER}' --format '{{{{.Names}}}}'", shell=True).decode().strip().split('\n'): + logger.error(f"Контейнер '{DOCKER_CONTAINER}' не найден.") return False + subprocess.check_call(f"docker exec {DOCKER_CONTAINER} test -f {WG_CONFIG_FILE}", shell=True) return True -async def periodic_ensure_peer_names(): - db.ensure_peer_names() - async def on_startup(dp): os.makedirs('files/connections', exist_ok=True) os.makedirs('users', exist_ok=True) - await load_isp_cache_task() - environment_ok = await check_environment() - if not environment_ok: - logger.error("Необходимо инициализировать AmneziaVPN перед запуском бота.") + await load_isp_cache() + if not await check_environment(): for admin_id in admins: - await bot.send_message(admin_id, "Необходимо инициализировать AmneziaVPN перед запуском бота.") + await bot.send_message(admin_id, "Ошибка инициализации AmneziaVPN.") await bot.close() sys.exit(1) - admins = db.get_admins() # Загружаем актуальный список админов из базы - if not admins: - logger.error("Список администраторов пуст.") + if not db.get_admins(): + logger.error("Список админов пуст.") sys.exit(1) - if not scheduler.running: - scheduler.add_job(update_all_clients_traffic, IntervalTrigger(minutes=1)) - scheduler.add_job(periodic_ensure_peer_names, IntervalTrigger(minutes=1)) - scheduler.start() - logger.info("Планировщик запущен для обновления трафика каждую минуту.") - users = db.get_users_with_expiration() - for user in users: - client_name, expiration_time, traffic_limit = user - if expiration_time: - try: - expiration_datetime = datetime.fromisoformat(expiration_time) - except ValueError: - logger.error(f"Некорректный формат даты для пользователя {client_name}: {expiration_time}") - continue - if expiration_datetime.tzinfo is None: - expiration_datetime = expiration_datetime.replace(tzinfo=pytz.UTC) - if expiration_datetime > datetime.now(pytz.UTC): - scheduler.add_job( - deactivate_user, - trigger=DateTrigger(run_date=expiration_datetime), - args=[client_name], - id=client_name - ) - logger.info(f"Запланирована деактивация пользователя {client_name} на {expiration_datetime}") - else: - await deactivate_user(client_name) + scheduler.add_job(db.ensure_peer_names, IntervalTrigger(minutes=1)) async def on_shutdown(dp): - if scheduler.running: - scheduler.shutdown() - logger.info("Планировщик остановлен.") + scheduler.shutdown() if __name__ == '__main__': executor.start_polling(dp, on_startup=on_startup, on_shutdown=on_shutdown)