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