diff --git a/client.py b/client.py index c490b222..7b699364 100644 --- a/client.py +++ b/client.py @@ -69,10 +69,17 @@ def extend_client_key(session, tg_id, client_id, email: str, new_expiry_time: in if not client_data: print("Не удалось получить данные клиента.") return False + + # Получаем текущий срок действия + current_expiry_time = client_data.get('expiryTime', 0) - # Обновляем данные клиента - client_data['expiryTime'] = new_expiry_time - + # Если нет текущего срока, используем новый срок + if current_expiry_time == 0: + current_expiry_time = new_expiry_time + + # Определяем новый срок окончания + updated_expiry_time = max(current_expiry_time, new_expiry_time) + # Формируем данные для обновления payload = { "id": 1, # Если id динамически изменяется, замените это значение @@ -81,14 +88,14 @@ def extend_client_key(session, tg_id, client_id, email: str, new_expiry_time: in { "id": client_id, "alterId": 0, - "email": client_data['email'], - "limitIp": 1, - # "totalGB": 2, - "expiryTime": new_expiry_time, - "enable": client_data['enable'], + "email": email.lower(), # Приведение email к нижнему регистру + "limitIp": 2, + "totalGB": 429496729600000, + "expiryTime": updated_expiry_time, + "enable": True, "tgId": tg_id, - "subId": '', - "flow": 'xtls-rprx-vision' + "subId": "", + "flow": "xtls-rprx-vision" } ] }) diff --git a/database.py b/database.py index 68e7752d..eae9071c 100644 --- a/database.py +++ b/database.py @@ -89,4 +89,10 @@ async def update_balance(tg_id: int, amount: float): SET balance = balance + ? WHERE tg_id = ? ''', (amount, tg_id)) - await db.commit() \ No newline at end of file + await db.commit() + +async def get_key_count(tg_id: int) -> int: + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute('SELECT COUNT(*) FROM keys k JOIN connections c ON k.client_id = c.client_id WHERE c.tg_id = ?', (tg_id,)) as cursor: + count = await cursor.fetchone() + return count[0] if count else 0 diff --git a/handlers/__pycache__/keys.cpython-310.pyc b/handlers/__pycache__/keys.cpython-310.pyc index a374ce33..db258c30 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 e6aecc61..78eea876 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 17b82b22..a59a06fb 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 e2dc771c..e27b2c4f 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 2df51c0e..689b49da 100644 --- a/handlers/keys.py +++ b/handlers/keys.py @@ -2,16 +2,23 @@ from aiogram import types, Router import aiosqlite from bot import bot from datetime import datetime, timedelta -from database import get_active_key_email, get_balance, store_key, update_balance -from client import extend_client_key +from database import get_balance, update_balance from client import extend_client_key, login_with_credentials -from config import ADMIN_ID, ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_PATH +from config import ADMIN_USERNAME, DATABASE_PATH, ADMIN_PASSWORD router = Router() +# Обработка запроса на просмотр ключей @router.callback_query(lambda c: c.data == 'view_keys') async def process_callback_view_keys(callback_query: types.CallbackQuery): tg_id = callback_query.from_user.id + + # Создаем клавиатуру + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text='Инструкции по использованию', callback_data='instructions')], + [types.InlineKeyboardButton(text='Продлить ключ', callback_data='renew_key')], + [types.InlineKeyboardButton(text='Назад', callback_data='view_profile')] + ]) try: async with aiosqlite.connect(DATABASE_PATH) as db: @@ -22,33 +29,30 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery): WHERE c.tg_id = ? ''', (tg_id,)) as cursor: record = await cursor.fetchone() - + if record: - key = record[0] - 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') - renew_button = types.InlineKeyboardButton(text='Продлить ключ', callback_data='renew_key') - - keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ - [instructions_button], - [renew_button] - ]) - - await bot.send_message(tg_id, response_message, parse_mode="HTML", reply_markup=keyboard) + key, expiry_time = record + expiry_date = datetime.utcfromtimestamp(expiry_time / 1000) + current_date = datetime.utcnow() + days_left = (expiry_date - current_date).days + + days_left_message = f"Осталось дней: {days_left}" if days_left > 0 else "Ключ истек." + response_message = (f"Ваш ключ:\n
{key}
\n" + f"Дата окончания: {expiry_date.strftime('%Y-%m-%d %H:%M:%S')}\n" + f"{days_left_message}") + else: response_message = "У вас нет ключей." - await bot.send_message(tg_id, response_message, reply_to_message_id=callback_query.message.message_id) - + + await delete_previous_message(callback_query) + await bot.send_message(tg_id, response_message, parse_mode="HTML", reply_markup=keyboard) + 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 handle_error(tg_id, callback_query, f"Ошибка при получении ключей: {e}") + await callback_query.answer() +# Обработка запроса на продление ключа @router.callback_query(lambda c: c.data == 'renew_key') async def process_callback_renew_key(callback_query: types.CallbackQuery): tg_id = callback_query.from_user.id @@ -57,22 +61,71 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery): async with aiosqlite.connect(DATABASE_PATH) as db: async with db.execute('SELECT client_id, email, expiry_time FROM connections WHERE tg_id = ?', (tg_id,)) as cursor: record = await cursor.fetchone() - + if record: - client_id = record[0] - email = record[1] # Получаем email - expiry_time = record[2] + client_id, email, expiry_time = record current_time = datetime.utcnow().timestamp() * 1000 - new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000) if expiry_time <= current_time: await callback_query.message.answer("Ваш ключ уже истек и не может быть продлен.") return + # Создаем клавиатуру для выбора плана продления + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ + [types.InlineKeyboardButton(text='Продлить на 1 месяц (100 руб.)', callback_data='renew_1_month')], + [types.InlineKeyboardButton(text='Продлить на 3 месяца (250 руб.)', callback_data='renew_3_months')], + [types.InlineKeyboardButton(text='Назад', callback_data='view_profile')] + ]) + + balance = await get_balance(tg_id) + response_message = (f"Выберите план продления:\n" + f"Баланс: {balance} руб.\n" + f"Действующий ключ истекает {datetime.utcfromtimestamp(expiry_time / 1000).strftime('%Y-%m-%d %H:%M:%S')}") + + await delete_previous_message(callback_query) + await bot.send_message(tg_id, response_message, parse_mode="HTML", reply_markup=keyboard) + + except Exception as e: + await callback_query.message.answer(f"Ошибка при выборе плана: {e}") + + await callback_query.answer() + +# Обработка выбора плана продления +@router.callback_query(lambda c: c.data.startswith('renew_')) +async def process_callback_renew_plan(callback_query: types.CallbackQuery): + tg_id = callback_query.from_user.id + plan = callback_query.data.split('_')[1] # '1' или '3' + days_to_extend = 30 * int(plan) # 30 дней или 90 дней + + try: + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute('SELECT client_id, email, expiry_time FROM connections WHERE tg_id = ?', (tg_id,)) as cursor: + record = await cursor.fetchone() + + if record: + client_id = record[0] + email = record[1] + expiry_time = record[2] + current_time = datetime.utcnow().timestamp() * 1000 + + if expiry_time <= current_time: + await callback_query.message.answer("Ваш ключ уже истек и не может быть продлен.") + return + + # Рассчитываем новый срок окончания, добавляя дни в зависимости от выбранного плана + new_expiry_time = int(expiry_time + timedelta(days=days_to_extend).total_seconds() * 1000) + + # Определяем стоимость продления + cost = 100 if plan == '1' else 250 + # Проверка баланса balance = await get_balance(tg_id) - if balance < 100: - await callback_query.message.answer("Недостаточно средств для продления ключа.") + if balance < cost: + replenish_button = types.InlineKeyboardButton(text='Пополнить баланс', callback_data='replenish_balance') + back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys') + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[replenish_button], [back_button]]) + + await callback_query.message.answer("Недостаточно средств для продления ключа.", reply_markup=keyboard) return # Создаем сессию для API-запросов @@ -82,16 +135,30 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery): success = extend_client_key(session, tg_id, client_id, email, new_expiry_time) if success: - await update_balance(tg_id, -100) # Списание 100 рублей с баланса + await update_balance(tg_id, -cost) # Списание средств с баланса await db.execute('UPDATE connections SET expiry_time = ? WHERE client_id = ?', (new_expiry_time, client_id)) await db.commit() - await callback_query.message.answer("Ваш ключ был успешно продлен на месяц.") + response_message = f"Ваш ключ был успешно продлен на {days_to_extend // 30} месяц(-)." + back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys') + keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) + await bot.send_message(tg_id, response_message, reply_markup=keyboard) else: - await callback_query.message.answer("Ошибка при продлении ключа.") + await bot.send_message(tg_id, "Ошибка при продлении ключа.") else: - await callback_query.message.answer("У вас нет ключей для продления.") + await bot.send_message(tg_id, "У вас нет ключей для продления.") except Exception as e: - await callback_query.message.answer(f"Ошибка при продлении ключа: {e}") + await bot.send_message(tg_id, f"Ошибка при продлении ключа: {e}") - await callback_query.answer() \ No newline at end of file + await callback_query.answer() + + +# Удаление предыдущего сообщения +async def delete_previous_message(callback_query: types.CallbackQuery): + if callback_query.message.message_id: + await bot.delete_message(chat_id=callback_query.from_user.id, message_id=callback_query.message.message_id) + +# Обработка ошибок +async def handle_error(tg_id, callback_query, message): + await delete_previous_message(callback_query) + await bot.send_message(tg_id, message) diff --git a/handlers/pay.py b/handlers/pay.py index bf99bba6..cb6d4376 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -4,111 +4,209 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup import aiosqlite from config import DATABASE_PATH, ADMIN_ID -from database import get_balance, update_balance +from database import get_balance, update_balance, get_key_count from bot import bot +from handlers.profile import process_callback_view_profile router = Router() +# Определение состояний class ReplenishBalanceState(StatesGroup): choosing_transfer_method = State() + choosing_amount = State() waiting_for_admin_confirmation = State() +async def send_message_with_deletion(chat_id, text, reply_markup=None, state=None, message_key='last_message_id'): + """ + Отправляет сообщение и удаляет предыдущее, если оно существует. + """ + if state: + try: + state_data = await state.get_data() + previous_message_id = state_data.get(message_key) + + if previous_message_id: + await bot.delete_message(chat_id=chat_id, message_id=previous_message_id) + + sent_message = await bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup) + await state.update_data({message_key: sent_message.message_id}) + + except Exception as e: + print(f"Ошибка при удалении/отправке сообщения: {e}") + return None + + return sent_message + @router.callback_query(lambda c: c.data == 'replenish_balance') async def process_callback_replenish_balance(callback_query: types.CallbackQuery, state: FSMContext): + tg_id = callback_query.from_user.id + + key_count = await get_key_count(tg_id) + if key_count <= 0: + await send_message_with_deletion( + tg_id, + "У вас нет ключей. Пополнение баланса возможно только при наличии ключа.", + state=state, + message_key='key_error_message_id' + ) + create_key_button = InlineKeyboardButton(text='Создать ключ', callback_data='create_key') + keyboard = InlineKeyboardMarkup(inline_keyboard=[[create_key_button]]) + + await callback_query.message.edit_text( + text="Пожалуйста, создайте ключ для продолжения.", + reply_markup=keyboard + ) + return + 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_requisites')], + [InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_profile')] ]) - await bot.send_message(callback_query.from_user.id, "Выберите метод перевода:", reply_markup=keyboard) - await callback_query.answer() # Уведомляем Telegram, что запрос обработан + + await callback_query.message.edit_text( + text="Выберите метод перевода:", + reply_markup=keyboard + ) + await callback_query.answer() + +@router.callback_query(lambda c: c.data == 'back_to_profile') +async def back_to_profile_handler(callback_query: types.CallbackQuery, state: FSMContext): + await process_callback_view_profile(callback_query, state) @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 bot.send_message(callback_query.from_user.id, "Неверные данные для выбора метода перевода.") + await send_message_with_deletion(callback_query.from_user.id, "Неверные данные для выбора метода перевода.", state=state, message_key='transfer_method_error_message_id') return - transfer_method = data[2] # 'requisites' + transfer_method = data[2] if transfer_method == 'requisites': - message = ( - "Банк: Т-Банк\n" - "2200701036597224\n" - "Получатель: Лисицын В.Д.\n" - f"\n" - "После перевода отправьте чек и дождитесь подтверждения." - ) + amount_keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text='100 RUB', callback_data='amount_100')], + [InlineKeyboardButton(text='300 RUB', callback_data='amount_300')], + [InlineKeyboardButton(text='500 RUB', callback_data='amount_500')], + [InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_profile')] + ]) - # Удаляем старое сообщение о выборе метода - await bot.edit_message_text( - text="Вы выбрали метод перевода. Теперь вы увидите реквизиты.", - chat_id=callback_query.from_user.id, - message_id=callback_query.message.message_id + await callback_query.message.edit_text( + text="Выберите сумму пополнения:", + reply_markup=amount_keyboard ) - - # Отправляем реквизиты в новом сообщении - await bot.send_message(callback_query.from_user.id, message) - + await state.update_data(transfer_method=transfer_method) + await state.set_state(ReplenishBalanceState.choosing_amount) else: - await bot.send_message(callback_query.from_user.id, "Неверный метод перевода.") + await send_message_with_deletion(callback_query.from_user.id, "Неверный метод перевода.", state=state, message_key='transfer_method_error_message_id') + +@router.callback_query(lambda c: c.data.startswith('amount_')) +async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): + data = callback_query.data.split('_', 1) + + if len(data) != 2: + await send_message_with_deletion(callback_query.from_user.id, "Неверные данные для выбора суммы.", state=state, message_key='amount_error_message_id') return - - await state.update_data(transfer_method=transfer_method) + + amount_str = data[1] + amount = int(amount_str) + + # Удаляем сообщение с кнопками выбора суммы + state_data = await state.get_data() + amount_selection_message_id = state_data.get('amount_selection_message_id') + + if amount_selection_message_id: + try: + await bot.delete_message(chat_id=callback_query.from_user.id, message_id=amount_selection_message_id) + except Exception as e: + print(f"Ошибка при удалении сообщения: {e}") + + await state.update_data(amount=amount) await state.set_state(ReplenishBalanceState.waiting_for_admin_confirmation) + transfer_method = state_data.get('transfer_method') + message = ( + "Банк: Т-Банк\n" + "2200701036597224\n" + "Получатель: Лисицын В.Д.\n" + "\n" + "После перевода отправьте чек и дождитесь подтверждения." + ) + + await callback_query.message.edit_text(text=message, reply_markup=None) + 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}')] + [InlineKeyboardButton(text='Подтвердить', callback_data=f'admin_confirm_{callback_query.from_user.id}_{transfer_method}_{amount}')], + [InlineKeyboardButton(text='Отклонить', callback_data=f'admin_decline_{callback_query.from_user.id}_{transfer_method}_{amount}')] ]) - + admin_message = ( f"Пользователь {callback_query.from_user.full_name} запросил пополнение баланса.\n" f"Метод перевода: {transfer_method}\n" + f"Сумма пополнения: {amount} RUB\n" "Пожалуйста, подтвердите или отклоните запрос." ) - - await bot.send_message(ADMIN_ID, admin_message, reply_markup=admin_keyboard) + + await send_message_with_deletion(ADMIN_ID, admin_message, reply_markup=admin_keyboard, state=state, message_key='admin_request_message_id') 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 bot.send_message(callback_query.from_user.id, "Вы не являетесь администратором.") + await send_message_with_deletion(callback_query.from_user.id, "Вы не являетесь администратором.", state=state, message_key='admin_error_message_id') return data = callback_query.data.split('_', 4) if len(data) < 4: - await bot.send_message(callback_query.from_user.id, "Неверные данные для обработки запроса.") + await send_message_with_deletion(callback_query.from_user.id, "Неверные данные для обработки запроса.", state=state, message_key='admin_error_message_id') return - action = data[1] # 'confirm' или 'decline' - user_id_str = data[2] + action = data[1] + user_id = int(data[2]) transfer_method = data[3] + amount = int(data[4]) try: - user_id = int(user_id_str) # Преобразование user_id в целое число - if action == 'confirm': - 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 bot.send_message(callback_query.from_user.id, f"Баланс пользователя успешно пополнен на {amount} RUB.\nТекущий баланс: {balance}") + + await send_message_with_deletion(callback_query.from_user.id, f"Баланс пользователя успешно пополнен на {amount} RUB.\nТекущий баланс: {balance}", state=state, message_key='admin_confirm_message_id') await bot.send_message(user_id, f"Ваш баланс был успешно пополнен на {amount} RUB.") + # Удаляем сообщение с реквизитами + state_data = await state.get_data() + requisites_message_id = state_data.get('requisites_message_id') + if requisites_message_id: + try: + await bot.delete_message(chat_id=user_id, message_id=requisites_message_id) + except Exception as e: + print(f"Ошибка при удалении сообщения с реквизитами: {e}") + + profile_button = InlineKeyboardButton(text='Профиль', callback_data='view_profile') + profile_keyboard = InlineKeyboardMarkup(inline_keyboard=[[profile_button]]) + await bot.send_message(user_id, "После пополнения баланса вы можете просмотреть свой профиль.", reply_markup=profile_keyboard) + elif action == 'decline': - await bot.send_message(callback_query.from_user.id, "Пополнение баланса отклонено.") + await send_message_with_deletion(callback_query.from_user.id, "Пополнение баланса отклонено.", state=state, message_key='admin_decline_message_id') await bot.send_message(user_id, "Ваш запрос на пополнение баланса был отклонен.") except Exception as e: - await bot.send_message(callback_query.from_user.id, f"Ошибка при пополнении баланса: {e}") - print(f"Ошибка при пополнении баланса: {e}") # Отладочное сообщение в консоль + await send_message_with_deletion(callback_query.from_user.id, f"Ошибка при пополнении баланса: {e}", state=state, message_key='admin_error_message_id') + print(f"Ошибка при пополнении баланса: {e}") await state.clear() await callback_query.answer() + +@router.message(lambda m: m.text and m.text.startswith('Недостаточно средств для продления')) +async def handle_insufficient_funds(message: types.Message): + user_id = message.from_user.id + replenish_keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text='Пополнить баланс', callback_data='replenish_balance')] + ]) + await send_message_with_deletion(user_id, "Ваш баланс недостаточен для продления ключа. Пожалуйста, пополните баланс.", reply_markup=replenish_keyboard, state=None, message_key='insufficient_funds_message_id') diff --git a/handlers/profile.py b/handlers/profile.py index b39c6a8e..c5fb2432 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -5,7 +5,7 @@ from aiogram.fsm.state import State, StatesGroup from datetime import datetime import aiosqlite from config import DATABASE_PATH -from database import get_balance, has_active_key +from database import get_balance, get_key_count router = Router() @@ -15,50 +15,45 @@ class ReplenishBalanceState(StatesGroup): async def process_callback_view_profile(callback_query: types.CallbackQuery, state: FSMContext): tg_id = callback_query.from_user.id - username = callback_query.from_user.username try: - # Проверяем наличие активного ключа - email = await has_active_key(tg_id) - expiry_date = "Нет активного ключа" - - if email: - async with aiosqlite.connect(DATABASE_PATH) as db: - async with db.execute( - "SELECT expiry_time FROM connections WHERE tg_id = ? AND expiry_time > ?", - (tg_id, int(datetime.utcnow().timestamp() * 1000)) - ) as cursor: - record = await cursor.fetchone() - if record: - expiry_date = datetime.utcfromtimestamp(record[0] / 1000).strftime("%Y-%m-%d %H:%M:%S") + # Получаем количество ключей + key_count = await get_key_count(tg_id) + # Получаем баланс balance = await get_balance(tg_id) + if balance is None: + balance = 0 # По умолчанию 0, если баланс неизвестен + profile_message = ( - f"Профиль \n\n" - f"ID: {tg_id}\n" - f"Ключ: {email if expiry_date else 'Нет активного ключа'}\n" - f"Баланс: {balance} RUB\n" + f"Профиль пользователя\n\n" + f"🔹 ID: {tg_id}\n" + f"🔹 Количество ключей: {key_count}\n" + f"🔹 Баланс: {balance} RUB\n" + f"🔹 Дата регистрации: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')}\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') + button_back = InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_menu') # Добавляем кнопку "Назад" + keyboard = InlineKeyboardMarkup(inline_keyboard=[ [button_create_key], # Добавляем кнопку "Создать ключ" [button_view_keys], - [button_replenish_balance] + [button_replenish_balance], + [button_back] # Добавляем кнопку "Назад" в клавиатуру ]) except Exception as e: - profile_message = f"Ошибка при получении данных профиля: {e}" + profile_message = f"❗️ Ошибка при получении данных профиля: {e}" keyboard = None - # Отправляем сообщение с клавиатурой - await callback_query.message.bot.send_message( - chat_id=callback_query.message.chat.id, + # Редактируем существующее сообщение, а не отправляем новое + await callback_query.message.edit_text( text=profile_message, - parse_mode='Markdown', + parse_mode='HTML', reply_markup=keyboard ) await callback_query.answer() diff --git a/handlers/start.py b/handlers/start.py index b9b9dab0..af3521cc 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -4,7 +4,7 @@ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, CallbackQu 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 config import API_TOKEN, ADMIN_ID, CHANNEL_URL from bot import dp, bot # Убедитесь, что bot и dp импортируются из правильного места router = Router() @@ -22,20 +22,22 @@ async def start_command(message: types.Message): "Нажмите на соответствующую кнопку ниже, чтобы начать!" ) - 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') + 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') + button_channel = InlineKeyboardButton(text='📢 Наш канал', url=CHANNEL_URL) inline_keyboard = InlineKeyboardMarkup(inline_keyboard=[ [button_view_profile], [button_about_vpn], - [button_feedback] + [button_feedback], + [button_channel] ]) reply_keyboard = ReplyKeyboardMarkup( keyboard=[ - [KeyboardButton(text="В начало")], - [KeyboardButton(text="Мой профиль")] + [KeyboardButton(text="Меню")], + # [KeyboardButton(text="Мой профиль")] ], resize_keyboard=True ) @@ -61,18 +63,72 @@ async def handle_about_vpn(callback_query: CallbackQuery): "Каждому пользователю предоставляется индивидуальный ключ для подключения. " "Этот ключ необходим для использования нашего VPN-сервиса." ) - await callback_query.message.answer( + + # Кнопка "Назад" + button_back = InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_menu') + + # Обновлённая клавиатура с кнопкой "Назад" + inline_keyboard_back = InlineKeyboardMarkup(inline_keyboard=[ + [button_back] + ]) + + await callback_query.message.edit_text( info_message, - parse_mode='Markdown' + parse_mode='Markdown', + reply_markup=inline_keyboard_back # Добавление клавиатуры с кнопкой "Назад" + ) + await callback_query.answer() + +@router.callback_query(lambda c: c.data == 'back_to_menu') +async def handle_back_to_menu(callback_query: CallbackQuery): + welcome_text = ( + "*Добро пожаловать в наш сервис!*\n\n" + "Вы можете воспользоваться следующими функциями:\n\n" + "🔑 *Создать ключ для подключения VPN* - Получите уникальный ключ для доступа к VPN.\n" + "📅 *Узнать дату окончания ключа* - Проверьте, когда истекает срок действия вашего ключа.\n" + "👤 *Просмотреть ваш профиль* - Получите информацию о вашем аккаунте и балансе.\n\n" + "Нажмите на соответствующую кнопку ниже, чтобы начать!" + ) + + 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') + button_channel = InlineKeyboardButton(text='📢 Наш канал', url=CHANNEL_URL) + + inline_keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [button_view_profile], + [button_about_vpn], + [button_feedback], + [button_channel] + ]) + + # Редактирование сообщения на главное меню + await callback_query.message.edit_text( + welcome_text, + parse_mode='Markdown', + reply_markup=inline_keyboard ) 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' + feedback_text = "Напишите нам, если у вас возникли трудности с подключением, есть отзыв или предложение. @pocomacho" + + # Кнопка "Назад" + button_back = InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_menu') + + # Обновлённая клавиатура с кнопкой "Назад" + inline_keyboard_back = InlineKeyboardMarkup(inline_keyboard=[ + [button_back] + ]) + + # Редактирование сообщения с кнопкой "Назад" + await callback_query.message.edit_text( + feedback_text, + parse_mode='Markdown', + reply_markup=inline_keyboard_back # Клавиатура с кнопкой "Назад" ) + await state.set_state(FeedbackState.waiting_for_feedback) await state.update_data(user_id=callback_query.from_user.id) await callback_query.answer() @@ -95,3 +151,8 @@ async def receive_feedback(message: types.Message, state: FSMContext): print(f"Ошибка при отправке обратной связи: {e}") # Логирование ошибок await state.clear() + +# Пример обработки команды старт +@router.message(Command('start')) +async def cmd_start(message: Message): + await start_command(message) \ No newline at end of file diff --git a/key_management.py b/key_management.py index 5efb7877..3f45b3f5 100644 --- a/key_management.py +++ b/key_management.py @@ -1,63 +1,52 @@ -from aiogram import Bot, Dispatcher, Router, F, types -from aiogram.filters import Command -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 -from auth import login_with_credentials, link -from client import add_client from datetime import datetime, timedelta -from config import API_TOKEN, ADMIN_PASSWORD, ADMIN_USERNAME, ADMIN_CHAT_ID, DATABASE_PATH -from database import add_connection, has_active_key, get_active_key_email, get_balance, store_key, update_balance -from client import extend_client_key +import asyncio +import re import uuid import aiosqlite -import re +from aiogram import Bot, Dispatcher, Router, types, F +from aiogram.filters import Command +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery, Message +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup + +from auth import login_with_credentials, link +from client import add_client, extend_client_key +from config import API_TOKEN, ADMIN_PASSWORD, ADMIN_USERNAME, ADMIN_CHAT_ID, DATABASE_PATH +from database import add_connection, has_active_key, get_balance, store_key, update_balance from bot import dp from handlers.start import start_command -from bot import bot from handlers.profile import process_callback_view_profile -import asyncio - +from handlers.keys import process_callback_view_keys +from bot import bot router = Router() -def escape_markdown(text: str) -> str: - """ - Экранирование специальных символов для Markdown. - """ - 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() +# Обработка нажатия кнопки создания ключа @dp.callback_query(F.data == 'create_key') -async def process_callback_create_key(callback_query: types.CallbackQuery, state: FSMContext): - await bot.send_message(callback_query.from_user.id, "Введите имя вашего профиля VPN:") +async def process_callback_create_key(callback_query: CallbackQuery, state: FSMContext): + await callback_query.message.edit_text("Вам будет выдан пробный ключ. Пожалуйста, выберите имя для вашего ключа:") await state.set_state(Form.waiting_for_key_name) await callback_query.answer() +# Обработка текстовых сообщений @dp.message() -async def handle_text(message: types.Message, state: FSMContext): +async def handle_text(message: Message, state: FSMContext): current_state = await state.get_state() print(f"Received message: {message.text}, Current state: {current_state}") + # Обработка команд и переходов if message.text == "Мой профиль": - # Создайте фейковый объект CallbackQuery для вызова функции callback_query = types.CallbackQuery( - id="1", # Используйте уникальный идентификатор + id="1", from_user=message.from_user, chat_instance='', data='view_profile', @@ -66,103 +55,59 @@ async def handle_text(message: types.Message, state: FSMContext): await process_callback_view_profile(callback_query, state) return - if message.text == "/start": - await start_command(message) - return - - if message.text == "В начало": + if message.text in ["/start", "Меню"]: await start_command(message) return + # Если ожидается имя ключа if current_state == Form.waiting_for_key_name.state: - tg_id = message.from_user.id - key_name = sanitize_key_name(message.text) - tg_name = message.from_user.username + await handle_key_name_input(message, state) - if not key_name: - await bot.send_message(tg_id, "Имя клиента не указано.") - await state.clear() - return +# Обработка ввода имени ключа +async def handle_key_name_input(message: Message, state: FSMContext): + tg_id = message.from_user.id + key_name = sanitize_key_name(message.text) - if await has_active_key(tg_id): - await bot.send_message(tg_id, "У вас уже есть активный ключ. Вы не можете создать новый.") - await state.clear() - return - - # Сохраняем информацию о запросе в контексте состояния - await state.update_data(key_name=key_name, tg_id=tg_id) - - # Отправляем запрос на подтверждение админу - admin_message = ( - f"Новый запрос на создание ключа:\n" - f"Телеграм-ID: {tg_id}\n" - f"Имя ключа: {key_name}\n" - f"Имя пользователя Telegram: @{tg_name}\n" - f"Введите 'yes' для подтверждения или 'no' для отклонения." - ) - keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='Да', callback_data=f'confirm_key_{tg_id}_{key_name}')], - [InlineKeyboardButton(text='Нет', callback_data=f'reject_key_{tg_id}')] - ]) - await bot.send_message(ADMIN_CHAT_ID, admin_message, reply_markup=keyboard) - - await bot.send_message(tg_id, "Ожидайте подтверждения администратора.") + if not key_name: + await message.reply("Имя профиля не указано. Введите имя на английском языке.") await state.clear() - -@dp.callback_query(F.data.startswith('confirm_key_')) -async def handle_admin_confirmation(callback_query: CallbackQuery, state: FSMContext): - if callback_query.message.chat.id != int(ADMIN_CHAT_ID): return - + + if await has_active_key(tg_id): + await message.reply("У вас уже есть активный ключ. Вы не можете создать новый.") + await state.clear() + return + + await state.update_data(key_name=key_name, tg_id=tg_id) + session = login_with_credentials(ADMIN_USERNAME, ADMIN_PASSWORD) - data = callback_query.data.split('_', 3) - tg_id = int(data[2]) - key_name = data[3] + client_id = str(uuid.uuid4()) + email = key_name.lower() + current_time = datetime.utcnow() + expiry_time = int((current_time + timedelta(days=1)).timestamp() * 1000) try: - client_id = str(uuid.uuid4()) - email = key_name.lower() # Приведение email к нижнему регистру - limit_ip = 1 - total_gb = 0 - current_time = datetime.utcnow() - - # Определяем время истечения ключа - if await has_active_key(tg_id): - # Если есть активный ключ, устанавливаем срок действия на 30 дней - expiry_time = int((current_time + timedelta(days=30)).timestamp() * 1000) - else: - # Первый ключ устанавливаем на 1 день - expiry_time = int((current_time + timedelta(days=1)).timestamp() * 1000) - - enable = True - 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) - + # Создание клиента и получение ссылки + add_client(session, client_id, email, tg_id, limit_ip=1, total_gb=0, expiry_time=expiry_time, enable=True, flow="xtls-rprx-vision") 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 add_connection(tg_id, client_id, email, expiry_time, 0) + await store_key(client_id, email, connection_link) - # Создаем кнопки - instructions_button = InlineKeyboardButton(text='Инструкции по использованию', callback_data='instructions') - keyboard = InlineKeyboardMarkup(inline_keyboard=[[instructions_button]]) + keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text='Инструкции по использованию', callback_data='instructions')], + [InlineKeyboardButton(text='Перейти в профиль', callback_data='view_profile')] + ]) - # Отправляем сообщение с ключом и кнопкой 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, "Ключ создан и отправлен клиенту.") + await message.reply(key_message, parse_mode="HTML", reply_markup=keyboard) except Exception as e: - await bot.send_message(callback_query.message.chat.id, f"Ошибка при создании ключа: {e}") - print(f"Ошибка при создании ключа: {e}") # Отладочное сообщение в консоль - - await callback_query.answer() + await message.reply(f"Ошибка при создании ключа: {e}") + print(f"Ошибка при создании ключа: {e}") + await state.clear() +# Обработка кнопки "Инструкции" @dp.callback_query(F.data == 'instructions') async def handle_instructions(callback_query: CallbackQuery): instructions_message = ( @@ -175,12 +120,29 @@ async def handle_instructions(callback_query: CallbackQuery): "4. Выберите 'Вставить из буфера обмена' для добавления ключа.\n\n" "Если у вас возникнут вопросы, не стесняйтесь обращаться в поддержку." ) - await callback_query.message.answer( - instructions_message, - parse_mode='Markdown' - ) + + back_button = InlineKeyboardButton(text='Назад', callback_data='back_to_main') + keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]]) + + await callback_query.message.edit_text(instructions_message, parse_mode='Markdown', reply_markup=keyboard) await callback_query.answer() +# Обработка кнопки "Назад" +@dp.callback_query(F.data == 'back_to_main') +async def handle_back_to_main(callback_query: CallbackQuery): + tg_id = callback_query.from_user.id + new_callback_query = CallbackQuery( + id=callback_query.id, + from_user=callback_query.from_user, + chat_instance=callback_query.chat_instance, + data='view_keys', + message=callback_query.message + ) + + await process_callback_view_keys(new_callback_query) + await callback_query.answer() + +# Фоновая задача для продления ключей async def renew_expired_keys(): while True: current_time = datetime.utcnow() @@ -192,16 +154,20 @@ async def renew_expired_keys(): balance = await get_balance(tg_id) if balance >= 100: new_expiry_time = int((current_time + timedelta(days=30)).timestamp() * 1000) - + async with aiosqlite.connect(DATABASE_PATH) as db: await db.execute('UPDATE connections SET expiry_time = ? WHERE tg_id = ? AND client_id = ?', (new_expiry_time, tg_id, client_id)) await db.commit() await update_balance(tg_id, -100) await extend_client_key(client_id) - + print(f"Ключ для клиента {client_id} продлен на месяц и списано 100 рублей.") else: - print(f"Недостаточно средств на балансе для клиента {client_id}.") + print(f"Недостаточно средств на балансе для клиента {client_id}. Предложение пополнить баланс.") + replenish_keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text='Пополнить баланс', callback_data='replenish_balance')] + ]) + await bot.send_message(tg_id, "Ваш баланс недостаточен для продления ключа. Пожалуйста, пополните баланс.", reply_markup=replenish_keyboard) - await asyncio.sleep(3600) \ No newline at end of file + await asyncio.sleep(3600)