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)