diff --git a/handlers/__pycache__/keys.cpython-310.pyc b/handlers/__pycache__/keys.cpython-310.pyc index 1c9d0b6e..5d50a89d 100644 Binary files a/handlers/__pycache__/keys.cpython-310.pyc and b/handlers/__pycache__/keys.cpython-310.pyc differ diff --git a/handlers/__pycache__/pay.cpython-310.pyc b/handlers/__pycache__/pay.cpython-310.pyc index 544e555d..e6aecc61 100644 Binary files a/handlers/__pycache__/pay.cpython-310.pyc and b/handlers/__pycache__/pay.cpython-310.pyc differ diff --git a/handlers/__pycache__/profile.cpython-310.pyc b/handlers/__pycache__/profile.cpython-310.pyc index b038020a..17b82b22 100644 Binary files a/handlers/__pycache__/profile.cpython-310.pyc and b/handlers/__pycache__/profile.cpython-310.pyc differ diff --git a/handlers/__pycache__/start.cpython-310.pyc b/handlers/__pycache__/start.cpython-310.pyc index 395da8ca..e2dc771c 100644 Binary files a/handlers/__pycache__/start.cpython-310.pyc and b/handlers/__pycache__/start.cpython-310.pyc differ diff --git a/handlers/keys.py b/handlers/keys.py index 331f0764..b545e2aa 100644 --- a/handlers/keys.py +++ b/handlers/keys.py @@ -25,11 +25,18 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery): expiry_time = record[1] expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S") response_message = f"Ваш ключ:\n
{key}
\nДата окончания: {expiry_date}" + + # Добавляем кнопку с инструкциями + instructions_button = types.InlineKeyboardButton(text='Инструкции по использованию', callback_data='instructions') + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[instructions_button]]) + + await bot.send_message(tg_id, response_message, parse_mode="HTML", reply_markup=keyboard) else: response_message = "У вас нет ключей." + await bot.send_message(tg_id, response_message, reply_to_message_id=callback_query.message.message_id) except Exception as e: response_message = f"Ошибка при получении ключей: {e}" + await bot.send_message(tg_id, response_message, reply_to_message_id=callback_query.message.message_id) - await bot.send_message(tg_id, response_message, parse_mode="HTML", reply_to_message_id=callback_query.message.message_id) await callback_query.answer() diff --git a/handlers/pay.py b/handlers/pay.py index 9c06aeea..bf99bba6 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -17,100 +17,98 @@ class ReplenishBalanceState(StatesGroup): async def process_callback_replenish_balance(callback_query: types.CallbackQuery, state: FSMContext): await state.set_state(ReplenishBalanceState.choosing_transfer_method) keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='По реквизитам', callback_data='transfer_method_requisites')], - [InlineKeyboardButton(text='По СБП', callback_data='transfer_method_sbp')] + [InlineKeyboardButton(text='По реквизитам', callback_data='transfer_method_requisites')] ]) - await callback_query.message.answer("Выберите метод перевода:", reply_markup=keyboard) - await callback_query.answer() + await bot.send_message(callback_query.from_user.id, "Выберите метод перевода:", reply_markup=keyboard) + await callback_query.answer() # Уведомляем Telegram, что запрос обработан @router.callback_query(lambda c: c.data.startswith('transfer_method_')) async def process_transfer_method_selection(callback_query: types.CallbackQuery, state: FSMContext): data = callback_query.data.split('_', 2) if len(data) != 3: - await callback_query.answer("Неверные данные для выбора метода перевода.") + await bot.send_message(callback_query.from_user.id, "Неверные данные для выбора метода перевода.") return - transfer_method = data[2] # 'requisites' или 'sbp' + transfer_method = data[2] # 'requisites' if transfer_method == 'requisites': message = ( - "Реквизиты для пополнения:\n" - f'\n' - "Банк: Пример Банк\n" - "Расчетный счет: 12345678901234567890\n" - "Получатель: ИП Иванов Иван Иванович" - f'\n' - "после перевода отправьте чек и ждите подтверждения" + "Банк: Т-Банк\n" + "2200701036597224\n" + "Получатель: Лисицын В.Д.\n" + f"\n" + "После перевода отправьте чек и дождитесь подтверждения." ) - elif transfer_method == 'sbp': - message = ( - "СБП для пополнения:\n" - f'\n' - "Номер банка: 1234567890\n" - "ФИО: Иванов Иван Иванович\n" - f'\n' - "после перевода отправьте чек и ждите подтверждения" + + # Удаляем старое сообщение о выборе метода + await bot.edit_message_text( + text="Вы выбрали метод перевода. Теперь вы увидите реквизиты.", + chat_id=callback_query.from_user.id, + message_id=callback_query.message.message_id ) + + # Отправляем реквизиты в новом сообщении + await bot.send_message(callback_query.from_user.id, message) + else: - await callback_query.answer("Неверный метод перевода.") + await bot.send_message(callback_query.from_user.id, "Неверный метод перевода.") return - await callback_query.message.answer(message) + await state.update_data(transfer_method=transfer_method) + await state.set_state(ReplenishBalanceState.waiting_for_admin_confirmation) + + admin_keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text='Подтвердить', callback_data=f'admin_confirm_{callback_query.from_user.id}_{transfer_method}')], + [InlineKeyboardButton(text='Отклонить', callback_data=f'admin_decline_{callback_query.from_user.id}_{transfer_method}')] + ]) + + admin_message = ( + f"Пользователь {callback_query.from_user.full_name} запросил пополнение баланса.\n" + f"Метод перевода: {transfer_method}\n" + "Пожалуйста, подтвердите или отклоните запрос." + ) + + await bot.send_message(ADMIN_ID, admin_message, reply_markup=admin_keyboard) await callback_query.answer() @router.callback_query(lambda c: c.data.startswith('admin_')) async def process_admin_confirmation(callback_query: types.CallbackQuery, state: FSMContext): if callback_query.from_user.id != ADMIN_ID: - await callback_query.answer("Вы не являетесь администратором.") + await bot.send_message(callback_query.from_user.id, "Вы не являетесь администратором.") return data = callback_query.data.split('_', 4) if len(data) < 4: - await callback_query.answer("Неверные данные для обработки запроса.") + await bot.send_message(callback_query.from_user.id, "Неверные данные для обработки запроса.") return action = data[1] # 'confirm' или 'decline' - user_id_str = data[3] + user_id_str = data[2] + transfer_method = data[3] try: user_id = int(user_id_str) # Преобразование user_id в целое число if action == 'confirm': - - amount_str = data[4].split('_')[0] - - # Убедимся, что amount_str действительно является числом - if amount_str.isdigit(): - amount = int(amount_str) # Преобразование amount в целое число - else: - await callback_query.answer("Неверная сумма для пополнения. Проверьте формат данных.") - print(f"Ошибка преобразования суммы: amount_str='{amount_str}'") # Отладочное сообщение в консоль - return + amount = 100 # Пример суммы, на которую нужно пополнить баланс async with aiosqlite.connect(DATABASE_PATH) as db: - # Обновляем баланс пользователя await update_balance(user_id, amount) await db.commit() balance = await get_balance(user_id) - await callback_query.message.answer(f"Баланс пользователя успешно пополнен на {amount} единиц.\nТекущий баланс: {balance}") - - # Сообщаем пользователю о пополнении + await bot.send_message(callback_query.from_user.id, f"Баланс пользователя успешно пополнен на {amount} RUB.\nТекущий баланс: {balance}") await bot.send_message(user_id, f"Ваш баланс был успешно пополнен на {amount} RUB.") elif action == 'decline': - await callback_query.message.answer("Пополнение баланса отклонено.") - - # Сообщаем пользователю об отклонении + await bot.send_message(callback_query.from_user.id, "Пополнение баланса отклонено.") await bot.send_message(user_id, "Ваш запрос на пополнение баланса был отклонен.") except Exception as e: - # Отладочное сообщение - await callback_query.message.answer(f"Ошибка при пополнении баланса: {e}") + await bot.send_message(callback_query.from_user.id, f"Ошибка при пополнении баланса: {e}") print(f"Ошибка при пополнении баланса: {e}") # Отладочное сообщение в консоль - # Очистка состояния await state.clear() await callback_query.answer() diff --git a/handlers/profile.py b/handlers/profile.py index 03eefefd..b39c6a8e 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -35,24 +35,32 @@ async def process_callback_view_profile(callback_query: types.CallbackQuery, sta balance = await get_balance(tg_id) profile_message = ( f"Профиль \n\n" - f"ID: @{tg_id}\n" - f"Ключ: {email if email else 'Нет активного ключа'}\n" + f"ID: {tg_id}\n" + f"Ключ: {email if expiry_date else 'Нет активного ключа'}\n" f"Баланс: {balance} RUB\n" ) # Кнопки для действий в профиле + button_create_key = InlineKeyboardButton(text='Создать ключ', callback_data='create_key') button_view_keys = InlineKeyboardButton(text='Мои ключи', callback_data='view_keys') button_replenish_balance = InlineKeyboardButton(text='Пополнить баланс', callback_data='replenish_balance') - keyboard = InlineKeyboardMarkup(inline_keyboard=[[button_view_keys], [button_replenish_balance]]) + keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [button_create_key], # Добавляем кнопку "Создать ключ" + [button_view_keys], + [button_replenish_balance] + ]) - profile_message += "\nВы можете просмотреть ваши ключи или пополнить баланс ниже:" - except Exception as e: profile_message = f"Ошибка при получении данных профиля: {e}" keyboard = None # Отправляем сообщение с клавиатурой - await callback_query.message.reply(profile_message, reply_markup=keyboard) + await callback_query.message.bot.send_message( + chat_id=callback_query.message.chat.id, + text=profile_message, + parse_mode='Markdown', + reply_markup=keyboard + ) await callback_query.answer() @router.callback_query(lambda c: c.data == 'view_profile') diff --git a/handlers/start.py b/handlers/start.py index ddac6a2e..b9b9dab0 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -1,31 +1,37 @@ -from aiogram import types, Router +from aiogram import Bot, Dispatcher, Router, types from aiogram.filters import Command -from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, KeyboardButton +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery, Message, ReplyKeyboardMarkup, KeyboardButton +from aiogram.fsm.storage.memory import MemoryStorage +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from config import API_TOKEN, ADMIN_ID +from bot import dp, bot # Убедитесь, что bot и dp импортируются из правильного места router = Router() -async def start_command(message: types.Message, callback_data: str = None): +class FeedbackState(StatesGroup): + waiting_for_feedback = State() + +async def start_command(message: types.Message): welcome_text = ( - "Добро пожаловать! Вы можете создать ключ для подключения VPN, " - "просмотреть статистику использования, узнать дату окончания ключа или " - "просмотреть ваш профиль." + "*Добро пожаловать в наш сервис!*\n\n" + "Вы можете воспользоваться следующими функциями:\n\n" + "🔑 *Создать ключ для подключения VPN* - Получите уникальный ключ для доступа к VPN.\n" + "📅 *Узнать дату окончания ключа* - Проверьте, когда истекает срок действия вашего ключа.\n" + "👤 *Просмотреть ваш профиль* - Получите информацию о вашем аккаунте и балансе.\n\n" + "Нажмите на соответствующую кнопку ниже, чтобы начать!" ) - # Создаем кнопки для команд - button_create_key = InlineKeyboardButton(text='Создать ключ', callback_data='create_key') -# button_view_stats = InlineKeyboardButton(text='Посмотреть статистику', callback_data='view_stats') -# button_view_expiry = InlineKeyboardButton(text='Дата окончания ключа', callback_data='view_expiry') button_view_profile = InlineKeyboardButton(text='Мой профиль', callback_data='view_profile') + button_about_vpn = InlineKeyboardButton(text='О VPN', callback_data='about_vpn') + button_feedback = InlineKeyboardButton(text='Обратная связь', callback_data='feedback') - # Создаем клавиатуру с кнопками inline_keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [button_create_key], -# [button_view_stats], -# [button_view_expiry], - [button_view_profile] + [button_view_profile], + [button_about_vpn], + [button_feedback] ]) - # Создаем кнопку для возврата в начало reply_keyboard = ReplyKeyboardMarkup( keyboard=[ [KeyboardButton(text="В начало")], @@ -34,9 +40,58 @@ async def start_command(message: types.Message, callback_data: str = None): resize_keyboard=True ) - # Отправляем сообщение с кнопками - await message.reply(welcome_text, reply_markup=inline_keyboard) - await message.answer( - "", + await message.bot.send_message( + chat_id=message.chat.id, + text=welcome_text, + parse_mode='Markdown', + reply_markup=inline_keyboard + ) + + await message.bot.send_message( + chat_id=message.chat.id, + text="Выберите действие:", reply_markup=reply_keyboard - ) \ No newline at end of file + ) + +@router.callback_query(lambda c: c.data == 'about_vpn') +async def handle_about_vpn(callback_query: CallbackQuery): + info_message = ( + "*О VPN*\n\n" + "Мы используем VLESS для обеспечения безопасного и надежного подключения к интернету. " + "Каждому пользователю предоставляется индивидуальный ключ для подключения. " + "Этот ключ необходим для использования нашего VPN-сервиса." + ) + await callback_query.message.answer( + info_message, + parse_mode='Markdown' + ) + await callback_query.answer() + +@router.callback_query(lambda c: c.data == 'feedback') +async def handle_feedback(callback_query: CallbackQuery, state: FSMContext): + await callback_query.message.answer( + "Напишите нам, если у вас возникли трудности с подключением, есть отзыв или предложение. Ваше сообщение будет отправлено админу.", + parse_mode='Markdown' + ) + await state.set_state(FeedbackState.waiting_for_feedback) + await state.update_data(user_id=callback_query.from_user.id) + await callback_query.answer() + +@router.message(FeedbackState.waiting_for_feedback) +async def receive_feedback(message: types.Message, state: FSMContext): + user_data = await state.get_data() + user_id = user_data.get('user_id') + + feedback_message = ( + f"Обратная связь от пользователя {user_id}:\n\n" + f"{message.text}" + ) + + try: + await bot.send_message(ADMIN_ID, feedback_message) + await message.answer("Спасибо за ваше сообщение! Мы свяжемся с вами, если это будет необходимо.") + except Exception as e: + await message.answer("Произошла ошибка при отправке вашего сообщения.") + print(f"Ошибка при отправке обратной связи: {e}") # Логирование ошибок + + await state.clear() diff --git a/key_management.py b/key_management.py index 5ba71195..98ca6ba7 100644 --- a/key_management.py +++ b/key_management.py @@ -1,6 +1,6 @@ from aiogram import Bot, Dispatcher, Router, F, types from aiogram.filters import Command -from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery, Message, ReplyKeyboardMarkup, KeyboardButton +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery, Message from aiogram.fsm.storage.memory import MemoryStorage from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup @@ -19,7 +19,6 @@ from handlers.profile import process_callback_view_profile router = Router() - def escape_markdown(text: str) -> str: """ Экранирование специальных символов для Markdown. @@ -27,21 +26,26 @@ def escape_markdown(text: str) -> str: escape_chars = r'_*[]()~`>#+-=|{}.!' return re.sub(r'([%s])' % re.escape(escape_chars), r'\\\1', text) +def sanitize_key_name(key_name: str) -> str: + """ + Очищает имя ключа от недопустимых символов и приводит его к нижнему регистру. + """ + # Оставляем только строчные буквы, цифры, символы @._- + return re.sub(r'[^a-z0-9@._-]', '', key_name.lower()) + class Form(StatesGroup): waiting_for_key_name = State() waiting_for_admin_confirmation = State() waiting_for_statistics = State() waiting_for_expiry_date = State() - viewing_profile = State() - + viewing_profile = State() @dp.callback_query(F.data == 'create_key') async def process_callback_create_key(callback_query: types.CallbackQuery, state: FSMContext): - await callback_query.message.reply("Введите имя вашего профиля VPN:") + await bot.send_message(callback_query.from_user.id, "Введите имя вашего профиля VPN:") await state.set_state(Form.waiting_for_key_name) await callback_query.answer() - @dp.message() async def handle_text(message: types.Message, state: FSMContext): current_state = await state.get_state() @@ -68,16 +72,16 @@ async def handle_text(message: types.Message, state: FSMContext): if current_state == Form.waiting_for_key_name.state: tg_id = message.from_user.id - key_name = message.text + key_name = sanitize_key_name(message.text) tg_name = message.from_user.username if not key_name: - await message.reply("Имя клиента не указано.") + await bot.send_message(tg_id, "Имя клиента не указано.") await state.clear() return if await has_active_key(tg_id): - await message.reply("У вас уже есть активный ключ. Вы не можете создать новый.") + await bot.send_message(tg_id, "У вас уже есть активный ключ. Вы не можете создать новый.") await state.clear() return @@ -98,8 +102,7 @@ async def handle_text(message: types.Message, state: FSMContext): ]) await bot.send_message(ADMIN_CHAT_ID, admin_message, reply_markup=keyboard) - await state.set_state(Form.waiting_for_admin_confirmation) - await message.reply("Ожидайте подтверждения администратора.") + await bot.send_message(tg_id, "Ожидайте подтверждения администратора.") await state.clear() @dp.callback_query(F.data.startswith('confirm_key_')) @@ -114,7 +117,7 @@ async def handle_admin_confirmation(callback_query: CallbackQuery, state: FSMCon try: client_id = str(uuid.uuid4()) - email = key_name + email = key_name.lower() # Приведение email к нижнему регистру limit_ip = 1 total_gb = 0 current_time = datetime.utcnow() @@ -123,25 +126,46 @@ async def handle_admin_confirmation(callback_query: CallbackQuery, state: FSMCon flow = "xtls-rprx-vision" balance = 0 + # Проверка данных перед созданием ключа + print(f"Creating key with client_id={client_id}, email={email}, tg_id={tg_id}") + add_client(session, client_id, email, tg_id, limit_ip, total_gb, expiry_time, enable, flow) connection_link = link(session, client_id, email) await add_connection(tg_id, client_id, email, expiry_time, balance) await store_key(client_id, email, connection_link) - await bot.send_message(tg_id, f"Ключ создан:\n
{connection_link}
", parse_mode="HTML") - await callback_query.message.reply("Ключ создан и отправлен клиенту.") + + # Создаем кнопки + instructions_button = InlineKeyboardButton(text='Инструкции по использованию', callback_data='instructions') + keyboard = InlineKeyboardMarkup(inline_keyboard=[[instructions_button]]) + + # Отправляем сообщение с ключом и кнопкой + key_message = f"Ключ создан:\n
{connection_link}
" + await bot.send_message(tg_id, key_message, parse_mode="HTML", reply_markup=keyboard) + await bot.send_message(callback_query.message.chat.id, "Ключ создан и отправлен клиенту.") except Exception as e: - await callback_query.message.reply(f"Ошибка при создании ключа: {e}") + await bot.send_message(callback_query.message.chat.id, f"Ошибка при создании ключа: {e}") + print(f"Ошибка при создании ключа: {e}") # Отладочное сообщение в консоль await callback_query.answer() -@dp.callback_query(F.data.startswith('reject_key_')) -async def handle_rejection(callback_query: CallbackQuery, state: FSMContext): - if callback_query.message.chat.id != int(ADMIN_CHAT_ID): - return - tg_id = int(callback_query.data.split('_', 3)[2]) - await bot.send_message(tg_id, "Создание ключа отклонено.") - await callback_query.message.reply("Запрос отклонен.") + +@dp.callback_query(F.data == 'instructions') +async def handle_instructions(callback_query: CallbackQuery): + instructions_message = ( + "*Инструкции по использованию вашего ключа:*\n\n" + "1. Скачайте приложение для вашего устройства:\n" + " - Для Android: [V2Ray](https://play.google.com/store/apps/details?id=com.v2ray.ang&hl=ru&pli=1)\n" + " - Для iPhone: [Streisand](https://apps.apple.com/ru/app/streisand/id6450534064)\n\n" + "2. Скопируйте предоставленный ключ, который вы получили ранее.\n" + "3. Откройте приложение и нажмите на плюсик сверху справа.\n" + "4. Выберите 'Вставить из буфера обмена' для добавления ключа.\n\n" + "Если у вас возникнут вопросы, не стесняйтесь обращаться в поддержку." + ) + await callback_query.message.answer( + instructions_message, + parse_mode='Markdown' + ) await callback_query.answer()