diff --git a/.gitignore b/.gitignore index 0b7bcc7b..fb38d4ca 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /database.db /bot_old.py /bot_old_2.py +/database.db \ No newline at end of file diff --git a/auth.py b/auth.py index c1d0880e..8827e109 100644 --- a/auth.py +++ b/auth.py @@ -19,6 +19,7 @@ def login_with_credentials(username, password): else: raise Exception(f"Ошибка авторизации: {response.status_code}, {response.text}") + def get_clients(session): response = session.get(GET_INBOUNDS_URL) if response.status_code == 200: diff --git a/bot.py b/bot.py index cdc9dc8b..3a8f1b84 100644 --- a/bot.py +++ b/bot.py @@ -8,7 +8,7 @@ storage = MemoryStorage() dp = Dispatcher(bot=bot, storage=storage) router = Router() -from handlers import start, profile, keys, stats, expiry, balance, pay +from handlers import start, profile, keys, balance, pay from key_management import router as key_management_router @@ -16,8 +16,6 @@ from key_management import router as key_management_router dp.include_router(start.router) dp.include_router(profile.router) dp.include_router(keys.router) -dp.include_router(stats.router) -dp.include_router(expiry.router) dp.include_router(balance.router) dp.include_router(key_management_router) dp.include_router(pay.router) diff --git a/client.py b/client.py index 308ad879..8522dc46 100644 --- a/client.py +++ b/client.py @@ -1,15 +1,10 @@ -import requests import json -from config import ADMIN_USERNAME, ADMIN_PASSWORD, GET_INBOUNDS_URL -import uuid def add_client(session, client_id: str, email: str, tg_id: str, limit_ip: int, total_gb: int, expiry_time: int, enable: bool, flow: str): url = 'https://solonet.pocomacho.ru:62553/solonet/panel/api/inbounds/addClient' - # Преобразуем email в нижний регистр email = email.lower() - # Формируем данные клиента client_data = { "id": client_id, "alterId": 0, @@ -23,22 +18,19 @@ def add_client(session, client_id: str, email: str, tg_id: str, limit_ip: int, t "flow": flow, } - # Преобразуем данные клиента в строку JSON settings = json.dumps({"clients": [client_data]}) - # Формируем тело запроса data = { - "id": 1, # Если id динамически изменяется, замените это значение + "id": 1, "settings": settings } headers = { - 'Content-Type': 'application/json', # Если требуется токен авторизации + 'Content-Type': 'application/json', } response = session.post(url, json=data, headers=headers) - # Выводим информацию о запросе и ответе print(f"Запрос на добавление клиента: {data}") print(f"Статус ответа: {response.status_code}") print(f"Ответ от сервера: {response.text}") @@ -48,7 +40,81 @@ def add_client(session, client_id: str, email: str, tg_id: str, limit_ip: int, t else: print(f"Ошибка при добавлении клиента: {response.status_code}, {response.text}") +def extend_client_key(session, tg_id, client_id, email: str, new_expiry_time: int) -> bool: + response = session.get(f"https://solonet.pocomacho.ru:62553/solonet/panel/api/inbounds/getClientTraffics/{email}") + print(f"GET {response.url} Status: {response.status_code}") + print(f"GET Response: {response.text}") + + if response.status_code != 200: + print(f"Ошибка при получении данных клиента: {response.status_code} - {response.text}") + return False + + client_data = response.json().get("obj", {}) + print(client_data) + if not client_data: + print("Не удалось получить данные клиента.") + return False -def generate_client_id(): - return str(uuid.uuid4()) + current_expiry_time = client_data.get('expiryTime', 0) + + if current_expiry_time == 0: + current_expiry_time = new_expiry_time + + updated_expiry_time = max(current_expiry_time, new_expiry_time) + + payload = { + "id": 1, + "settings": json.dumps({ + "clients": [ + { + "id": client_id, + "alterId": 0, + "email": email.lower(), + "limitIp": 2, + "totalGB": 429496729600000, + "expiryTime": updated_expiry_time, + "enable": True, + "tgId": tg_id, + "subId": "", + "flow": "xtls-rprx-vision" + } + ] + }) + } + + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + + try: + response = session.post(f"https://solonet.pocomacho.ru:62553/solonet/panel/api/inbounds/updateClient/{client_id}", json=payload, headers=headers) + print(f"POST {response.url} Status: {response.status_code}") + print(f"POST Request Data: {json.dumps(payload, indent=2)}") + print(f"POST Response: {response.text}") + + if response.status_code == 200: + return True + else: + print(f"Ошибка при продлении ключа: {response.status_code} - {response.text}") + return False + except Exception as e: + print(f"Ошибка запроса: {e}") + return False + +def delete_client(session, client_id: str) -> bool: + url = f"https://solonet.pocomacho.ru:62553/solonet/panel/api/inbounds/1/delClient/{client_id}" + headers = { + 'Accept': 'application/json' + } + try: + response = session.post(url, headers=headers) + if response.status_code == 200: + return True + else: + print(f"Ошибка при удалении клиента: {response.status_code} - {response.text}") + return False + except Exception as e: + print(f"Ошибка запроса: {e}") + return False \ No newline at end of file diff --git a/database.py b/database.py index 68e7752d..4e620f18 100644 --- a/database.py +++ b/database.py @@ -1,92 +1,84 @@ -import aiosqlite +import asyncpg from datetime import datetime -from config import DATABASE_PATH -import aiosqlite +from config import DATABASE_URL async def init_db(): - async with aiosqlite.connect(DATABASE_PATH) as db: - await db.execute(''' - CREATE TABLE IF NOT EXISTS connections ( - tg_id INTEGER NOT NULL, - client_id TEXT NOT NULL, - email TEXT NOT NULL, - expiry_time INTEGER NOT NULL, - balance REAL NOT NULL DEFAULT 0.0, -- Добавлено поле для баланса - PRIMARY KEY (tg_id, client_id) - ) - ''') - await db.execute(''' - CREATE TABLE IF NOT EXISTS keys ( - client_id TEXT NOT NULL, - email TEXT NOT NULL, - created_at INTEGER NOT NULL, - key TEXT NOT NULL, - PRIMARY KEY (client_id) - ) - ''') - await db.commit() + conn = await asyncpg.connect(DATABASE_URL) + await conn.execute(''' + CREATE TABLE IF NOT EXISTS connections ( + tg_id BIGINT PRIMARY KEY NOT NULL, + balance REAL NOT NULL DEFAULT 0.0, + trial INTEGER NOT NULL DEFAULT 0 + ) + ''') + await conn.execute(''' + CREATE TABLE IF NOT EXISTS keys ( + tg_id BIGINT NOT NULL, + client_id TEXT NOT NULL, + email TEXT NOT NULL, + created_at BIGINT NOT NULL, + expiry_time BIGINT NOT NULL, + key TEXT NOT NULL, + PRIMARY KEY (tg_id, client_id) + ) + ''') + await conn.close() -async def add_connection(tg_id: int, client_id: str, email: str, expiry_time: int, balance: float = 0.0): - async with aiosqlite.connect(DATABASE_PATH) as db: - await db.execute(''' - INSERT INTO connections (tg_id, client_id, email, expiry_time, balance) - VALUES (?, ?, ?, ?, ?) - ''', (tg_id, client_id, email, expiry_time, balance)) - await db.commit() +async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0): + conn = await asyncpg.connect(DATABASE_URL) + await conn.execute(''' + INSERT INTO connections (tg_id, balance, trial) + VALUES ($1, $2, $3) + ''', tg_id, balance, trial) + await conn.close() -async def store_key(client_id: str, email: str, key: str): - async with aiosqlite.connect(DATABASE_PATH) as db: - await db.execute(''' - INSERT INTO keys (client_id, email, created_at, key) - VALUES (?, ?, ?, ?) - ''', (client_id, email, int(datetime.utcnow().timestamp() * 1000), key)) - await db.commit() +async def store_key(tg_id: int, client_id: str, email: str, expiry_time: int, key: str): + conn = await asyncpg.connect(DATABASE_URL) + await conn.execute(''' + INSERT INTO keys (tg_id, client_id, email, created_at, expiry_time, key) + VALUES ($1, $2, $3, $4, $5, $6) + ''', tg_id, client_id, email, int(datetime.utcnow().timestamp() * 1000), expiry_time, key) + await conn.close() async def get_keys(tg_id: int): - async with aiosqlite.connect(DATABASE_PATH) as db: - async with db.execute(''' - SELECT client_id, email, created_at, key - FROM keys - WHERE tg_id = ? - ''', (tg_id,)) as cursor: - return await cursor.fetchall() - + conn = await asyncpg.connect(DATABASE_URL) + records = await conn.fetch(''' + SELECT client_id, email, created_at, key + FROM keys + WHERE tg_id = $1 + ''', tg_id) + await conn.close() + return records async def has_active_key(tg_id: int) -> bool: - async with aiosqlite.connect(DATABASE_PATH) as db: - async with db.execute("SELECT COUNT(*) FROM connections WHERE tg_id = ? AND expiry_time > ?", - (tg_id, int(datetime.utcnow().timestamp() * 1000))) as cursor: - count = await cursor.fetchone() - return count[0] > 0 + conn = await asyncpg.connect(DATABASE_URL) + count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE tg_id = $1", tg_id) + await conn.close() + return count > 0 -async def get_active_key_email(tg_id: int) -> str: - async with aiosqlite.connect(DATABASE_PATH) as db: - async with db.execute("SELECT email FROM connections WHERE tg_id = ? AND expiry_time > ?", - (tg_id, int(datetime.utcnow().timestamp() * 1000))) as cursor: - record = await cursor.fetchone() - return record[0] if record else None - -async def get_key_expiry_time(tg_id: int) -> datetime: - 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_time = record[0] - return datetime.utcfromtimestamp(expiry_time / 1000) - return None - -async def get_balance(tg_id: int) -> str: - async with aiosqlite.connect(DATABASE_PATH) as db: - async with db.execute("SELECT balance FROM connections WHERE tg_id = ?", (tg_id,)) as cursor: - record = await cursor.fetchone() - return record[0] if record else "Неизвестно" +async def get_balance(tg_id: int) -> float: + conn = await asyncpg.connect(DATABASE_URL) + balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) + await conn.close() + return balance if balance is not None else 0.0 async def update_balance(tg_id: int, amount: float): - async with aiosqlite.connect(DATABASE_PATH) as db: - await db.execute(''' - UPDATE connections - SET balance = balance + ? - WHERE tg_id = ? - ''', (amount, tg_id)) - await db.commit() \ No newline at end of file + conn = await asyncpg.connect(DATABASE_URL) + await conn.execute(''' + UPDATE connections + SET balance = balance + $1 + WHERE tg_id = $2 + ''', amount, tg_id) + await conn.close() + +async def get_trial(tg_id: int) -> int: + conn = await asyncpg.connect(DATABASE_URL) + trial = await conn.fetchval("SELECT trial FROM connections WHERE tg_id = $1", tg_id) + await conn.close() + return trial if trial is not None else 0 + +async def get_key_count(tg_id: int) -> int: + conn = await asyncpg.connect(DATABASE_URL) + count = await conn.fetchval('SELECT COUNT(*) FROM keys WHERE tg_id = $1', tg_id) + await conn.close() + return count if count is not None else 0 diff --git a/handlers/__pycache__/keys.cpython-310.pyc b/handlers/__pycache__/keys.cpython-310.pyc index 1c9d0b6e..f4b2f054 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..8cf0dcc0 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..a8600673 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..63472a32 100644 Binary files a/handlers/__pycache__/start.cpython-310.pyc and b/handlers/__pycache__/start.cpython-310.pyc differ diff --git a/handlers/expiry.py b/handlers/expiry.py deleted file mode 100644 index 71295447..00000000 --- a/handlers/expiry.py +++ /dev/null @@ -1,33 +0,0 @@ -from aiogram import types, Router -from database import get_active_key_email -from datetime import datetime -import aiosqlite -from config import DATABASE_PATH - -router = Router() - -@router.callback_query(lambda c: c.data == 'view_expiry') -async def process_callback_view_expiry(callback_query: types.CallbackQuery): - tg_id = callback_query.from_user.id - - try: - email = await get_active_key_email(tg_id) - 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_time = record[0] - expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S") - message_text = f"Дата окончания вашего ключа: {expiry_date}" - else: - message_text = "У вас нет активных ключей." - else: - message_text = "У вас нет активных ключей." - - except Exception as e: - message_text = f"Ошибка при получении даты окончания ключа: {e}" - - await callback_query.message.reply(message_text) - await callback_query.answer() diff --git a/handlers/keys.py b/handlers/keys.py index 331f0764..64875e1e 100644 --- a/handlers/keys.py +++ b/handlers/keys.py @@ -1,35 +1,257 @@ +import asyncpg from aiogram import types, Router -import aiosqlite -from config import DATABASE_PATH from bot import bot -from datetime import datetime +from datetime import datetime, timedelta +from database import get_balance, update_balance +from client import extend_client_key +from config import ADMIN_USERNAME, DATABASE_URL, ADMIN_PASSWORD +from auth import login_with_credentials +from client import extend_client_key, delete_client 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 - + try: - async with aiosqlite.connect(DATABASE_PATH) as db: - async with db.execute(''' - SELECT k.key, c.expiry_time - FROM keys k - JOIN connections c ON k.client_id = c.client_id - WHERE c.tg_id = ? - ''', (tg_id,)) as cursor: - record = await cursor.fetchone() + conn = await asyncpg.connect(DATABASE_URL) + try: + records = await conn.fetch(''' + SELECT email, client_id FROM keys WHERE tg_id = $1 + ''', tg_id) + + if records: + # Создаем кнопки для каждого ключа + buttons = [] + for record in records: + key_name = record['email'] + client_id = record['client_id'] + button = types.InlineKeyboardButton(text=key_name, callback_data=f'view_key_{key_name}_{client_id}') + buttons.append([button]) + + # Создаем клавиатуру с кнопками + inline_keyboard = types.InlineKeyboardMarkup(inline_keyboard=buttons) + response_message = "Выберите ключ для просмотра информации:" + + # Редактируем сообщение с клавиатурой + await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=inline_keyboard) + else: + # Если нет ключей, добавляем кнопку "Создать ключ" и "Назад" + response_message = "У вас нет ключей." + + # Кнопка "Создать ключ" + create_key_button = types.InlineKeyboardButton(text='Создать ключ', callback_data='create_key') + back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_profile') # Измените на правильное значение для кнопки "Назад" - 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}"
- else:
- response_message = "У вас нет ключей."
-
+ keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[create_key_button], [back_button]])
+
+ await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard)
+
+ finally:
+ await conn.close()
+
except Exception as e:
- response_message = f"Ошибка при получении ключей: {e}"
-
- await bot.send_message(tg_id, response_message, parse_mode="HTML", 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.startswith('view_key_'))
+async def process_callback_view_key(callback_query: types.CallbackQuery):
+ tg_id = callback_query.from_user.id
+ key_name, client_id = callback_query.data.split('_')[2], callback_query.data.split('_')[3]
+
+ try:
+ conn = await asyncpg.connect(DATABASE_URL)
+ try:
+ record = await conn.fetchrow('''
+ SELECT k.key, k.expiry_time
+ FROM keys k
+ WHERE k.tg_id = $1 AND k.email = $2
+ ''', tg_id, key_name)
+
+ if record:
+ key = record['key']
+ expiry_time = record['expiry_time']
+ 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}")
+
+ # Кнопки для продления, инструкций и удаления
+ renew_button = types.InlineKeyboardButton(text='Продлить ключ', callback_data=f'renew_key_{client_id}')
+ instructions_button = types.InlineKeyboardButton(text='Инструкции по использованию', callback_data='instructions')
+ delete_button = types.InlineKeyboardButton(text='Удалить ключ', callback_data=f'delete_key_{client_id}')
+ back_button = types.InlineKeyboardButton(text='Назад в профиль', callback_data='view_profile')
+ keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[renew_button], [instructions_button], [delete_button], [back_button]])
+
+ await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard, parse_mode="HTML")
+ else:
+ await bot.edit_message_text("Информация о ключе не найдена.", chat_id=tg_id, message_id=callback_query.message.message_id, parse_mode="HTML")
+
+ finally:
+ await conn.close()
+
+ except Exception as e:
+ await handle_error(tg_id, callback_query, f"Ошибка при получении информации о ключе: {e}")
+
+ await callback_query.answer()
+
+# Обработка запроса на удаление ключа
+@router.callback_query(lambda c: c.data.startswith('delete_key_'))
+async def process_callback_delete_key(callback_query: types.CallbackQuery):
+ tg_id = callback_query.from_user.id
+ client_id = callback_query.data.split('_')[2]
+
+ confirmation_keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
+ [types.InlineKeyboardButton(text='Да, удалить', callback_data=f'confirm_delete_{client_id}')],
+ [types.InlineKeyboardButton(text='Нет, отменить', callback_data='view_keys')]
+ ])
+
+ await bot.edit_message_text("Вы уверены, что хотите удалить ключ?", chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=confirmation_keyboard)
+ await callback_query.answer()
+
+@router.callback_query(lambda c: c.data.startswith('confirm_delete_'))
+async def process_callback_confirm_delete(callback_query: types.CallbackQuery):
+ tg_id = callback_query.from_user.id
+ client_id = callback_query.data.split('_')[2]
+
+ try:
+ conn = await asyncpg.connect(DATABASE_URL)
+ try:
+ record = await conn.fetchrow('SELECT email FROM keys WHERE client_id = $1', client_id)
+
+ if record:
+ email = record['email']
+
+ session = login_with_credentials(ADMIN_USERNAME, ADMIN_PASSWORD)
+ success = delete_client(session, client_id)
+
+ if success:
+ await conn.execute('DELETE FROM keys WHERE client_id = $1', client_id)
+ response_message = "Ключ был успешно удален."
+ else:
+ response_message = "Ошибка при удалении клиента через API."
+
+ else:
+ response_message = "Ключ не найден или уже удален."
+
+ back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys')
+ keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
+
+ await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard)
+
+ finally:
+ await conn.close()
+
+ except Exception as e:
+ await bot.edit_message_text(f"Ошибка при удалении ключа: {e}", chat_id=tg_id, message_id=callback_query.message.message_id)
+
+ await callback_query.answer()
+
+
+@router.callback_query(lambda c: c.data.startswith('renew_key_'))
+async def process_callback_renew_key(callback_query: types.CallbackQuery):
+ tg_id = callback_query.from_user.id
+ client_id = callback_query.data.split('_')[2]
+
+ try:
+ conn = await asyncpg.connect(DATABASE_URL)
+ try:
+ record = await conn.fetchrow('SELECT email, expiry_time FROM keys WHERE client_id = $1', client_id)
+
+ if record:
+ email = record['email']
+ expiry_time = record['expiry_time']
+ current_time = datetime.utcnow().timestamp() * 1000
+
+ if expiry_time <= current_time:
+ await bot.edit_message_text("Ваш ключ уже истек и не может быть продлен.", chat_id=tg_id, message_id=callback_query.message.message_id)
+ return
+
+ keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
+ [types.InlineKeyboardButton(text='Продлить на 1 месяц (100 руб.)', callback_data=f'renew_1_month_{client_id}')],
+ [types.InlineKeyboardButton(text='Продлить на 3 месяца (250 руб.)', callback_data=f'renew_3_months_{client_id}')],
+ [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 bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard, parse_mode="HTML")
+
+ finally:
+ await conn.close()
+
+ except Exception as e:
+ await bot.edit_message_text(f"Ошибка при выборе плана: {e}", chat_id=tg_id, message_id=callback_query.message.message_id)
+
+ 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, client_id = callback_query.data.split('_')[1], callback_query.data.split('_')[3] # '1' или '3' и client_id
+ days_to_extend = 30 * int(plan)
+
+ try:
+ conn = await asyncpg.connect(DATABASE_URL)
+ try:
+ record = await conn.fetchrow('SELECT email, expiry_time FROM keys WHERE client_id = $1', client_id)
+
+ if record:
+ email = record['email']
+ expiry_time = record['expiry_time']
+ current_time = datetime.utcnow().timestamp() * 1000
+
+ if expiry_time <= current_time:
+ await bot.edit_message_text("Ваш ключ уже истек и не может быть продлен.", chat_id=tg_id, message_id=callback_query.message.message_id)
+ 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 < 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 bot.edit_message_text("Недостаточно средств для продления ключа.", chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard)
+ return
+
+ session = login_with_credentials(ADMIN_USERNAME, ADMIN_PASSWORD)
+ success = extend_client_key(session, tg_id, client_id, email, new_expiry_time)
+
+ if success:
+ await update_balance(tg_id, -cost)
+ await conn.execute('UPDATE keys SET expiry_time = $1 WHERE client_id = $2', new_expiry_time, client_id)
+ 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.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard)
+ else:
+ await bot.edit_message_text("Ошибка при продлении ключа.", chat_id=tg_id, message_id=callback_query.message.message_id)
+ else:
+ await bot.edit_message_text("У вас нет ключей для продления.", chat_id=tg_id, message_id=callback_query.message.message_id)
+
+ finally:
+ await conn.close()
+
+ except Exception as e:
+ await bot.edit_message_text(f"Ошибка при продлении ключа: {e}", chat_id=tg_id, message_id=callback_query.message.message_id)
+
+ await callback_query.answer()
+
+# Обработка ошибок
+async def handle_error(tg_id, callback_query, message):
+ await bot.edit_message_text(message, chat_id=tg_id, message_id=callback_query.message.message_id)
+
diff --git a/handlers/pay.py b/handlers/pay.py
index 9c06aeea..0e1e5101 100644
--- a/handlers/pay.py
+++ b/handlers/pay.py
@@ -2,115 +2,242 @@ from aiogram import types, Router
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
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 config import ADMIN_ID, DATABASE_URL
+from database import get_balance, update_balance, get_key_count
from bot import bot
+from handlers.profile import process_callback_view_profile
+import asyncpg
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'):
+ """
+ Отправляет новое сообщение и удаляет предыдущее сообщение (если оно существует), сохраненное в состоянии FSM.
+ """
+ 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)
+ if state:
+ 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:
+ # Создаем кнопки "Создать ключ" и "Назад"
+ create_key_button = InlineKeyboardButton(text='Создать ключ', callback_data='create_key')
+ back_button = InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_profile')
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[[create_key_button, back_button]])
+
+ await callback_query.message.edit_text(
+ text="У вас нет ключей. Пополнение баланса возможно только при наличии ключа.\n"
+ "Пожалуйста, создайте ключ для продолжения.",
+ 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_sbp')]
+ [InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_profile')] # Кнопка "Назад"
])
- await callback_query.message.answer("Выберите метод перевода:", reply_markup=keyboard)
+
+ 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 callback_query.answer("Неверные данные для выбора метода перевода.")
+ 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' или 'sbp'
+ transfer_method = data[2]
if transfer_method == 'requisites':
- message = (
- "Реквизиты для пополнения:\n"
- f'\n'
- "Банк: Пример Банк\n"
- "Расчетный счет: 12345678901234567890\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 callback_query.message.edit_text(
+ text="Выберите сумму пополнения:",
+ reply_markup=amount_keyboard
)
- elif transfer_method == 'sbp':
- message = (
- "СБП для пополнения:\n"
- f'\n'
- "Номер банка: 1234567890\n"
- "ФИО: Иванов Иван Иванович\n"
- f'\n'
- "после перевода отправьте чек и ждите подтверждения"
- )
- else:
- await callback_query.answer("Неверный метод перевода.")
- return
+ await state.update_data(transfer_method=transfer_method)
+ await state.set_state(ReplenishBalanceState.choosing_amount)
- await callback_query.message.answer(message)
+ else:
+ await send_message_with_deletion(callback_query.from_user.id, "Неверный метод перевода.", state=state, message_key='transfer_method_error_message_id')
+ return
+
+
+@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
+
+ amount_str = data[1]
+ amount = int(amount_str)
+
+ # Получаем ID сообщения с кнопками выбора суммы
+ 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}_{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 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 callback_query.answer("Вы не являетесь администратором.")
+ 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 callback_query.answer("Неверные данные для обработки запроса.")
+ 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[3]
+ action = data[1]
+ user_id_str = data[2]
+ transfer_method = data[3]
+ amount = int(data[4])
try:
- user_id = int(user_id_str) # Преобразование user_id в целое число
+ user_id = int(user_id_str)
+
+ # Получаем данные из состояния для удаления сообщения с реквизитами
+ state_data = await state.get_data()
+ requisites_message_id = state_data.get('requisites_message_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
-
- async with aiosqlite.connect(DATABASE_PATH) as db:
+ # Подключаемся к базе данных PostgreSQL
+ conn = await asyncpg.connect(DATABASE_URL)
+ try:
# Обновляем баланс пользователя
await update_balance(user_id, amount)
- await db.commit()
- balance = await get_balance(user_id)
- await callback_query.message.answer(f"Баланс пользователя успешно пополнен на {amount} единиц.\nТекущий баланс: {balance}")
+ # Получаем обновленный баланс
+ balance = await get_balance(user_id)
- # Сообщаем пользователю о пополнении
- await bot.send_message(user_id, f"Ваш баланс был успешно пополнен на {amount} RUB.")
+ # Создаем клавиатуру с кнопкой "Профиль"
+ profile_button = InlineKeyboardButton(text='Профиль', callback_data='view_profile')
+ profile_keyboard = InlineKeyboardMarkup(inline_keyboard=[[profile_button]])
+
+ # Отправляем уведомление с кнопкой "Профиль" администратору
+ 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.",
+ reply_markup=profile_keyboard
+ )
+
+ # Удаляем сообщение с реквизитами
+ 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}")
+
+ finally:
+ # Закрываем соединение с базой данных
+ await conn.close()
elif action == 'decline':
- await callback_query.message.answer("Пополнение баланса отклонено.")
-
- # Сообщаем пользователю об отклонении
+ 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 callback_query.message.answer(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 03eefefd..6089ff66 100644
--- a/handlers/profile.py
+++ b/handlers/profile.py
@@ -3,9 +3,7 @@ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.fsm.context import FSMContext
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,44 +13,47 @@ 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 email 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')
- keyboard = InlineKeyboardMarkup(inline_keyboard=[[button_view_keys], [button_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_back] # Добавляем кнопку "Назад" в клавиатуру
+ ])
- profile_message += "\nВы можете просмотреть ваши ключи или пополнить баланс ниже:"
-
except Exception as e:
- profile_message = f"Ошибка при получении данных профиля: {e}"
+ profile_message = f"❗️ Ошибка при получении данных профиля: {e}"
keyboard = None
- # Отправляем сообщение с клавиатурой
- await callback_query.message.reply(profile_message, reply_markup=keyboard)
+ # Редактируем существующее сообщение, а не отправляем новое
+ await callback_query.message.edit_text(
+ text=profile_message,
+ parse_mode='HTML',
+ 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..b7e0fc55 100644
--- a/handlers/start.py
+++ b/handlers/start.py
@@ -1,42 +1,143 @@
-from aiogram import types, Router
+from aiogram import 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.context import FSMContext
+from aiogram.fsm.state import State, StatesGroup
+from config import ADMIN_ID, CHANNEL_URL
+from bot import 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_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_create_key],
-# [button_view_stats],
-# [button_view_expiry],
- [button_view_profile]
+ [button_view_profile],
+ [button_about_vpn],
+ [button_feedback],
+ [button_channel]
])
- # Создаем кнопку для возврата в начало
- reply_keyboard = ReplyKeyboardMarkup(
- keyboard=[
- [KeyboardButton(text="В начало")],
- [KeyboardButton(text="Мой профиль")]
- ],
- resize_keyboard=True
+ await message.bot.send_message(
+ chat_id=message.chat.id,
+ text=welcome_text,
+ parse_mode='Markdown',
+ reply_markup=inline_keyboard
)
- # Отправляем сообщение с кнопками
- await message.reply(welcome_text, reply_markup=inline_keyboard)
- await message.answer(
- "",
- 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-сервиса."
+ )
+
+ # Кнопка "Назад"
+ 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',
+ 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):
+ 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()
+
+@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()
+
+# Пример обработки команды старт
+@router.message(Command('start'))
+async def cmd_start(message: Message):
+ await start_command(message)
\ No newline at end of file
diff --git a/handlers/stats.py b/handlers/stats.py
deleted file mode 100644
index c0b1ae12..00000000
--- a/handlers/stats.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from aiogram import types, Router
-import re
-from database import get_active_key_email
-from auth import link, login_with_credentials
-from datetime import datetime
-import aiosqlite
-from config import DATABASE_PATH
-from config import ADMIN_USERNAME, ADMIN_PASSWORD
-
-
-router = Router()
-session = login_with_credentials(ADMIN_USERNAME, ADMIN_PASSWORD)
-
-@router.callback_query(lambda c: c.data == 'view_stats')
-async def process_callback_view_stats(callback_query: types.CallbackQuery):
- tg_id = callback_query.from_user.id
-
- try:
- email = await get_active_key_email(tg_id)
- if email:
- async with aiosqlite.connect(DATABASE_PATH) as db:
- async with db.execute("SELECT client_id FROM connections WHERE tg_id = ? AND expiry_time > ?",
- (tg_id, int(datetime.utcnow().timestamp() * 1000))) as cursor:
- record = await cursor.fetchone()
- if record:
- client_id = record[0]
- connection_link = link(session, client_id, email)
-
- up_match = re.search(r'up=(\d+)', connection_link)
- down_match = re.search(r'down=(\d+)', connection_link)
-
- up = up_match.group(1) if up_match else "Неизвестно"
- down = down_match.group(1) if down_match else "Неизвестно"
-
- statistics = f"Статистика вашего ключа:\nЗагрузка: {up} MB\nВыгрузка: {down} MB"
- else:
- statistics = "У вас нет активных ключей."
- else:
- statistics = "У вас нет активных ключей."
-
- except Exception as e:
- statistics = f"Ошибка при получении статистики: {e}"
-
- await callback_query.message.reply(f"Ваша статистика:\n{statistics}")
- await callback_query.answer()
diff --git a/key_management.py b/key_management.py
index 5ba71195..95b35bfa 100644
--- a/key_management.py
+++ b/key_management.py
@@ -1,55 +1,93 @@
-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.fsm.storage.memory import MemoryStorage
+import re
+import uuid
+import asyncio
+from datetime import datetime, timedelta
+from aiogram import Router, types, F
+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
-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
-import uuid
-import aiosqlite
-import re
-from bot import dp
-from handlers.start import start_command
-from bot import bot
+from config import API_TOKEN, ADMIN_PASSWORD, ADMIN_USERNAME, ADMIN_CHAT_ID, DATABASE_URL
+from database import add_connection, has_active_key, get_balance, store_key, update_balance
+from bot import dp, bot
from handlers.profile import process_callback_view_profile
+from handlers.start import start_command
+
+import asyncpg
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()
-
+ 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 state.set_state(Form.waiting_for_key_name)
+async def process_callback_create_key(callback_query: CallbackQuery, state: FSMContext):
+ tg_id = callback_query.from_user.id
+
+ # Получаем данные о trial из базы данных
+ conn = await asyncpg.connect(DATABASE_URL)
+ try:
+ existing_connection = await conn.fetchrow('SELECT trial FROM connections WHERE tg_id = $1', tg_id)
+ finally:
+ await conn.close()
+
+ trial_status = existing_connection['trial'] if existing_connection else 0
+
+ if trial_status == 1:
+ await callback_query.message.edit_text(
+ "У вас уже был пробный ключ. Вы можете создать новый за 100 рублей. "
+ "Хотите продолжить?",
+ reply_markup=InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(text='Да, создать новый ключ', callback_data='confirm_create_new_key')],
+ [InlineKeyboardButton(text='Назад', callback_data='cancel_create_key')]
+ ])
+ )
+ await state.update_data(creating_new_key=True)
+ else:
+ await callback_query.message.edit_text("Вам будет выдан пробный ключ. Пожалуйста, выберите имя для вашего ключа:")
+ await state.set_state(Form.waiting_for_key_name)
+
await callback_query.answer()
+@dp.callback_query(F.data == 'confirm_create_new_key')
+async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContext):
+ tg_id = callback_query.from_user.id
+ # Проверяем баланс перед созданием нового ключа
+ balance = await get_balance(tg_id)
+ if balance < 100:
+ replenish_button = InlineKeyboardButton(text='Перейти в профиль', callback_data='view_profile')
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]])
+ await callback_query.message.edit_text("Недостаточно средств на балансе для создания нового ключа.", reply_markup=keyboard)
+ await state.clear()
+ return
+
+ await callback_query.message.edit_text("Пожалуйста, выберите имя для вашего нового ключа:")
+ await state.set_state(Form.waiting_for_key_name)
+ await state.update_data(creating_new_key=True)
+
+ await callback_query.answer()
+
+@dp.callback_query(F.data == 'cancel_create_key')
+async def cancel_create_key(callback_query: CallbackQuery, state: FSMContext):
+ await process_callback_view_profile(callback_query, state)
+ 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()
-
+
if message.text == "Мой профиль":
- # Создайте фейковый объект CallbackQuery для вызова функции
callback_query = types.CallbackQuery(
- id="1", # Используйте уникальный идентификатор
+ id="1",
from_user=message.from_user,
chat_instance='',
data='view_profile',
@@ -58,90 +96,138 @@ 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 = message.text
- tg_name = message.from_user.username
+ await handle_key_name_input(message, state)
- if not key_name:
- await message.reply("Имя клиента не указано.")
- await state.clear()
- return
+async def handle_key_name_input(message: Message, state: FSMContext):
+ tg_id = message.from_user.id
- if await has_active_key(tg_id):
- await message.reply("У вас уже есть активный ключ. Вы не можете создать новый.")
- await state.clear()
- return
+ key_name = sanitize_key_name(message.text)
- # Сохраняем информацию о запросе в контексте состояния
- await state.update_data(key_name=key_name, tg_id=tg_id)
+ if not key_name:
+ await message.bot.send_message(tg_id, "Назовите профиль на английском языке.")
+ return # Прерываем выполнение функции
- # Отправляем запрос на подтверждение админу
- 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)
+ data = await state.get_data()
+ creating_new_key = data.get('creating_new_key', False)
- await state.set_state(Form.waiting_for_admin_confirmation)
- 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
-
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)
+
+ if creating_new_key:
+ # Проверяем, занято ли имя ключа
+ conn = await asyncpg.connect(DATABASE_URL)
+ try:
+ existing_key = await conn.fetchrow('SELECT * FROM keys WHERE email = $1', email)
+ if existing_key:
+ await message.bot.send_message(tg_id, "Это имя уже занято. Пожалуйста, выберите другое имя.")
+ return # Прерываем выполнение функции
+ finally:
+ await conn.close()
+
+ balance = await get_balance(tg_id)
+ if balance < 100:
+ replenish_button = InlineKeyboardButton(text='Перейти в профиль', callback_data='view_profile')
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]])
+ await message.bot.send_message(tg_id, "Недостаточно средств на балансе для создания нового ключа.", reply_markup=keyboard)
+ await state.clear()
+ return
+
+ await update_balance(tg_id, -100)
try:
- client_id = str(uuid.uuid4())
- email = key_name
- limit_ip = 1
- total_gb = 0
- current_time = datetime.utcnow()
- expiry_time = int((current_time + timedelta(days=30)).timestamp() * 1000)
- enable = True
- flow = "xtls-rprx-vision"
- balance = 0
-
- 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 bot.send_message(tg_id, f"Ключ создан:\n{connection_link}", parse_mode="HTML")
- await callback_query.message.reply("Ключ создан и отправлен клиенту.")
+ # Проверка существующей записи
+ conn = await asyncpg.connect(DATABASE_URL)
+ try:
+ existing_connection = await conn.fetchrow('SELECT * FROM connections WHERE tg_id = $1', tg_id)
+
+ if existing_connection:
+ await conn.execute('UPDATE connections SET trial = 1 WHERE tg_id = $1', tg_id)
+ else:
+ await add_connection(tg_id, 0, 1)
+
+ finally:
+ await conn.close()
+
+ await store_key(tg_id, client_id, email, expiry_time, connection_link)
+
+ keyboard = InlineKeyboardMarkup(inline_keyboard=[
+ [InlineKeyboardButton(text='Инструкции по использованию', callback_data='instructions')],
+ [InlineKeyboardButton(text='Перейти в профиль', callback_data='view_profile')]
+ ])
+
+ key_message = f"Ключ создан:\n{connection_link}"
+ await message.bot.send_message(tg_id, key_message, parse_mode="HTML", reply_markup=keyboard)
except Exception as e:
- await callback_query.message.reply(f"Ошибка при создании ключа: {e}")
+ await message.bot.send_message(tg_id, f"Ошибка при создании ключа: {e}")
+ await state.clear()
+
+
+@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"
+ "Если у вас возникнут вопросы, не стесняйтесь обращаться в поддержку."
+ )
+
+ 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.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 == 'back_to_main')
+async def handle_back_to_main(callback_query: CallbackQuery, state: FSMContext):
+ await process_callback_view_profile(callback_query, state)
await callback_query.answer()
+
+async def renew_expired_keys():
+ while True:
+ current_time = datetime.utcnow()
+ conn = await asyncpg.connect(DATABASE_URL)
+ try:
+ active_keys = await conn.fetch('SELECT tg_id FROM connections WHERE trial > 0')
+
+ finally:
+ await conn.close()
+
+ for record in active_keys:
+ tg_id = record['tg_id']
+ balance = await get_balance(tg_id)
+ if balance >= 100:
+ new_expiry_time = int((current_time + timedelta(days=30)).timestamp() * 1000)
+
+ conn = await asyncpg.connect(DATABASE_URL)
+ try:
+ await conn.execute('UPDATE keys SET expiry_time = $1 WHERE tg_id = $2', new_expiry_time, tg_id)
+ await update_balance(tg_id, -100)
+ finally:
+ await conn.close()
+
+ print(f"Ключ для пользователя {tg_id} продлен на месяц и списано 100 рублей.")
+ else:
+ print(f"Недостаточно средств на балансе для пользователя {tg_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)
+
diff --git a/path_to_your_database b/path_to_your_database
new file mode 100644
index 00000000..e69de29b