From 800a86c500da987ce832a385138cf812c44f25dd Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Sat, 9 Nov 2024 10:36:16 +0300 Subject: [PATCH] Refactoring and fix --- .gitignore | 63 +- auth.py | 51 +- backup.py | 31 +- bot.py | 10 +- client.py | 225 ++++---- database.py | 212 +++++-- handlers/admin/admin.py | 88 ++- handlers/admin/admin_panel.py | 164 ++++-- handlers/admin/user_editor.py | 345 +++++++---- handlers/commands.py | 88 ++- handlers/instructions/instructions.py | 19 +- handlers/keys/key_management.py | 195 +++++-- handlers/keys/keys.py | 541 +++++++++++++----- handlers/keys/subscriptions.py | 52 +- handlers/keys/trial_key.py | 68 ++- handlers/notifications.py | 276 ++++++--- .../{freekassa.py => freekassa_pay.py} | 119 ++-- handlers/payment/pay.py | 280 --------- handlers/payment/yookassa_pay.py | 340 +++++++++++ handlers/profile.py | 107 ++-- handlers/start.py | 135 +++-- handlers/utils.py | 18 +- middlewares/admin.py | 57 ++ 23 files changed, 2309 insertions(+), 1175 deletions(-) rename handlers/payment/{freekassa.py => freekassa_pay.py} (58%) delete mode 100644 handlers/payment/pay.py create mode 100644 handlers/payment/yookassa_pay.py create mode 100644 middlewares/admin.py diff --git a/.gitignore b/.gitignore index c66d53a6..70485cdc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,51 @@ -/venv -/__pycache__ -/vpn_users.db -/config.py -/database.db -/bot_old.py -/bot_old_2.py -/database.db -/backup_pg.sh -/config copy.py -/docker-compose.yml -__pycache__ -/handlers/texts.py +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +dist/ +build/ +*.egg-info/ + +# Virtual environments +venv/ +env/ +.env/ +.venv/ + +# IDE specific files +.idea/ +.vscode/ +*.sublime-project +*.sublime-workspace + +# Logs and databases +*.log +*.sqlite3 +*.db + +# Sensitive configuration files +config.py +config.ini +.env + +# Backup files +*.bak +*.swp +*~ + +# Specific project files +vpn_users.db +database.db +bot_old.py +bot_old_2.py +backup_pg.sh +docker-compose.yml +config copy.py +handlers/texts.py + +# Miscellaneous +.DS_Store +Thumbs.db diff --git a/auth.py b/auth.py index 207adf6c..ee0e238a 100644 --- a/auth.py +++ b/auth.py @@ -6,32 +6,34 @@ from config import SERVERS session = None + async def login_with_credentials(server_id: str, username: str, password: str): global session session = aiohttp.ClientSession() - api_url = SERVERS[server_id]['API_URL'] + api_url = SERVERS[server_id]["API_URL"] auth_url = f"{api_url}/login/" - - data = { - "username": username, - "password": password - } - + + data = {"username": username, "password": password} + async with session.post(auth_url, json=data) as response: if response.status == 200: session.cookie_jar.update_cookies(response.cookies) return session else: - raise Exception(f"Ошибка авторизации: {response.status}, {await response.text()}") + raise Exception( + f"Ошибка авторизации: {response.status}, {await response.text()}" + ) async def get_clients(session, server_id): - api_url = SERVERS[server_id]['API_URL'] - async with session.get(f'{api_url}/panel/api/inbounds/list/') as response: + api_url = SERVERS[server_id]["API_URL"] + async with session.get(f"{api_url}/panel/api/inbounds/list/") as response: if response.status == 200: return await response.json() else: - raise Exception(f"Ошибка при получении клиентов: {response.status}, {await response.text()}") + raise Exception( + f"Ошибка при получении клиентов: {response.status}, {await response.text()}" + ) async def link(session, server_id: str, client_id: str, email: str): @@ -42,29 +44,30 @@ async def link(session, server_id: str, client_id: str, email: str): :param email: str - электронная почта клиента :return: str - ссылка для подключения """ - response = await get_clients(session, server_id) - - if 'obj' not in response or len(response['obj']) == 0: + response = await get_clients(session, server_id) + + if "obj" not in response or len(response["obj"]) == 0: raise Exception("Не удалось получить данные клиентов.") - - inbounds = response['obj'][0] - settings = json.loads(inbounds['settings']) - - stream_settings = json.loads(inbounds['streamSettings']) - tcp = stream_settings.get('network', 'tcp') - reality = stream_settings.get('security', 'reality') - flow = stream_settings.get('flow', 'xtls-rprx-vision') - + + inbounds = response["obj"][0] + settings = json.loads(inbounds["settings"]) + + stream_settings = json.loads(inbounds["streamSettings"]) + tcp = stream_settings.get("network", "tcp") + reality = stream_settings.get("security", "reality") + flow = stream_settings.get("flow", "xtls-rprx-vision") + val = ( f"vless://{client_id}@{SERVERS[server_id]['DOMEN']}?type={tcp}&security={reality}&pbk={SERVERS[server_id]['PBK']}" f"&fp=chrome&sni={SERVERS[server_id]['SNI']}&sid={SERVERS[server_id]['SID']}=%2F&flow={flow}#{SERVERS[server_id]['PREFIX']}-{email}" ) return val + async def link_subscription(email, server_id): server = SERVERS.get(server_id) if server: subscription_url = f"{server['SUBSCRIPTION']}/{email}" return subscription_url else: - raise ValueError(f"Server '{server_id}' not found in configuration.") \ No newline at end of file + raise ValueError(f"Server '{server_id}' not found in configuration.") diff --git a/backup.py b/backup.py index 47ebd2d7..5a0baa94 100644 --- a/backup.py +++ b/backup.py @@ -17,12 +17,12 @@ async def backup_database(): DATE = datetime.now().strftime("%Y-%m-%d-%H%M%S") BACKUP_FILE = f"{BACKUP_DIR}/{DB_NAME}-backup-{DATE}.sql" - os.environ['PGPASSWORD'] = DB_PASSWORD + os.environ["PGPASSWORD"] = DB_PASSWORD try: subprocess.run( - ['pg_dump', '-U', USER, '-h', HOST, '-F', 'c', '-f', BACKUP_FILE, DB_NAME], - check=True + ["pg_dump", "-U", USER, "-h", HOST, "-F", "c", "-f", BACKUP_FILE, DB_NAME], + check=True, ) logging.info(f"Бэкап базы данных создан: {BACKUP_FILE}") except subprocess.CalledProcessError as e: @@ -30,8 +30,10 @@ async def backup_database(): return try: - with open(BACKUP_FILE, 'rb') as backup_file: - backup_input_file = BufferedInputFile(backup_file.read(), filename=os.path.basename(BACKUP_FILE)) + with open(BACKUP_FILE, "rb") as backup_file: + backup_input_file = BufferedInputFile( + backup_file.read(), filename=os.path.basename(BACKUP_FILE) + ) await bot.send_document(ADMIN_ID, backup_input_file) logging.info(f"Бэкап базы данных отправлен админу: {ADMIN_ID}") except Exception as e: @@ -39,11 +41,24 @@ async def backup_database(): try: subprocess.run( - ['find', BACKUP_DIR, '-type', 'f', '-name', '*.sql', '-mtime', '+7', '-exec', 'rm', '{}', ';'], - check=True + [ + "find", + BACKUP_DIR, + "-type", + "f", + "-name", + "*.sql", + "-mtime", + "+7", + "-exec", + "rm", + "{}", + ";", + ], + check=True, ) logging.info("Старые бэкапы удалены.") except subprocess.CalledProcessError as e: logging.error(f"Ошибка при удалении старых бэкапов: {e}") - del os.environ['PGPASSWORD'] + del os.environ["PGPASSWORD"] diff --git a/bot.py b/bot.py index fae3bb08..5fdfc304 100644 --- a/bot.py +++ b/bot.py @@ -11,7 +11,8 @@ router = Router() from handlers import commands, notifications, profile, start from handlers.admin import admin, admin_panel, user_editor from handlers.keys import key_management, keys -from handlers.payment import freekassa, pay +from handlers.payment import freekassa_pay, yookassa_pay +from middlewares.admin import AdminMiddleware dp.include_router(admin.router) dp.include_router(admin_panel.router) @@ -21,6 +22,9 @@ dp.include_router(start.router) dp.include_router(profile.router) dp.include_router(keys.router) dp.include_router(key_management.router) -dp.include_router(pay.router) -dp.include_router(freekassa.router) +dp.include_router(yookassa_pay.router) +dp.include_router(freekassa_pay.router) dp.include_router(notifications.router) + +dp.message.middleware(AdminMiddleware()) +dp.callback_query.middleware(AdminMiddleware()) diff --git a/client.py b/client.py index 04296be1..9e151d23 100644 --- a/client.py +++ b/client.py @@ -1,14 +1,28 @@ import json +import logging from config import SERVERS +logging.basicConfig(level=logging.DEBUG) + + +async def add_client( + session, + server_id: str, + client_id: str, + email: str, + tg_id: str, + limit_ip: int, + total_gb: int, + expiry_time: int, + enable: bool, + flow: str, +): + api_url = SERVERS[server_id]["API_URL"] + url = f"{api_url}/panel/api/inbounds/addClient" -async def add_client(session, server_id: str, client_id: str, email: str, tg_id: str, limit_ip: int, total_gb: int, expiry_time: int, enable: bool, flow: str): - api_url = SERVERS[server_id]['API_URL'] - url = f'{api_url}/panel/api/inbounds/addClient' - email = email.lower() - + client_data = { "id": client_id, "alterId": 0, @@ -21,53 +35,58 @@ async def add_client(session, server_id: str, client_id: str, email: str, tg_id: "subId": email, "flow": flow, } - + settings = json.dumps({"clients": [client_data]}) - data = { - "id": 1, - "settings": settings - } + data = {"id": 1, "settings": settings} headers = { - 'Content-Type': 'application/json', + "Content-Type": "application/json", } async with session.post(url, json=data, headers=headers) as response: - print(f"Запрос на добавление клиента: {data}") - print(f"Статус ответа: {response.status}") + logging.info(f"Запрос на добавление клиента: {data}") + logging.info(f"Статус ответа: {response.status}") response_text = await response.text() - print(f"Ответ от сервера: {response_text}") + logging.info(f"Ответ от сервера: {response_text}") if response.status == 200: - print(f"Клиент добавлен: email={email}") + logging.info(f"Клиент добавлен: email={email}") return await response.json() else: - print(f"Ошибка при добавлении клиента: {response.status}, {response_text}") + logging.error( + f"Ошибка при добавлении клиента: {response.status}, {response_text}" + ) return None -async def extend_client_key(session, server_id: str, tg_id, client_id, email: str, new_expiry_time: int) -> bool: - api_url = SERVERS[server_id]['API_URL'] - - async with session.get(f"{api_url}/panel/api/inbounds/getClientTraffics/{email}") as response: - print(f"GET {response.url} Status: {response.status}") +async def extend_client_key( + session, server_id: str, tg_id, client_id, email: str, new_expiry_time: int +) -> bool: + api_url = SERVERS[server_id]["API_URL"] + + async with session.get( + f"{api_url}/panel/api/inbounds/getClientTraffics/{email}" + ) as response: + logging.info(f"GET {response.url} Status: {response.status}") response_text = await response.text() - print(f"GET Response: {response_text}") - + logging.info(f"GET Response: {response_text}") + if response.status != 200: - print(f"Ошибка при получении данных клиента: {response.status} - {response_text}") + logging.error( + f"Ошибка при получении данных клиента: {response.status} - {response_text}" + ) return False - + client_data = (await response.json()).get("obj", {}) - print(client_data) + logging.info(client_data) if not client_data: - print("Не удалось получить данные клиента.") + logging.error("Не удалось получить данные клиента.") return False - current_expiry_time = client_data.get('expiryTime', 0) - + current_expiry_time = client_data.get("expiryTime", 0) + if current_expiry_time == 0: current_expiry_time = new_expiry_time @@ -75,7 +94,60 @@ async def extend_client_key(session, server_id: str, tg_id, client_id, email: st payload = { "id": 1, - "settings": json.dumps({ + "settings": json.dumps( + { + "clients": [ + { + "id": client_id, + "alterId": 0, + "email": email.lower(), + "limitIp": 2, + "totalGB": 0, + "expiryTime": updated_expiry_time, + "enable": True, + "tgId": tg_id, + "subId": email, + "flow": "xtls-rprx-vision", + } + ] + } + ), + } + + headers = {"Content-Type": "application/json", "Accept": "application/json"} + + try: + async with session.post( + f"{api_url}/panel/api/inbounds/updateClient/{client_id}", + json=payload, + headers=headers, + ) as response: + logging.info(f"POST {response.url} Status: {response.status}") + logging.info(f"POST Request Data: {json.dumps(payload, indent=2)}") + response_text = await response.text() + logging.info(f"POST Response: {response_text}") + + if response.status == 200: + return True + else: + logging.error( + f"Ошибка при продлении ключа: {response.status} - {response_text}" + ) + return False + except Exception as e: + logging.error(f"Ошибка запроса: {e}") + return False + + +async def extend_client_key_admin( + session, server_id: str, tg_id, client_id: str, email: str, new_expiry_time: int +) -> bool: + api_url = SERVERS[server_id]["API_URL"] + + payload = { + "id": 1, + "settings": json.dumps( + { "clients": [ { "id": client_id, @@ -83,95 +155,56 @@ async def extend_client_key(session, server_id: str, tg_id, client_id, email: st "email": email.lower(), "limitIp": 2, "totalGB": 0, - "expiryTime": updated_expiry_time, + "expiryTime": new_expiry_time, "enable": True, "tgId": tg_id, "subId": email, - "flow": "xtls-rprx-vision" + "flow": "xtls-rprx-vision", } ] - }) - } - - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - - try: - async with session.post(f"{api_url}/panel/api/inbounds/updateClient/{client_id}", json=payload, headers=headers) as response: - print(f"POST {response.url} Status: {response.status}") - print(f"POST Request Data: {json.dumps(payload, indent=2)}") - response_text = await response.text() - print(f"POST Response: {response_text}") - - if response.status == 200: - return True - else: - print(f"Ошибка при продлении ключа: {response.status} - {response_text}") - return False - except Exception as e: - print(f"Ошибка запроса: {e}") - return False + } + ), + } + + headers = {"Content-Type": "application/json", "Accept": "application/json"} -async def extend_client_key_admin(session, server_id: str, tg_id, client_id: str, email: str, new_expiry_time: int) -> bool: - api_url = SERVERS[server_id]['API_URL'] - - payload = { - "id": 1, - "settings": json.dumps({ - "clients": [ - { - "id": client_id, - "alterId": 0, - "email": email.lower(), - "limitIp": 2, - "totalGB": 0, - "expiryTime": new_expiry_time, - "enable": True, - "tgId": tg_id, - "subId": email, - "flow": "xtls-rprx-vision" - } - ] - }) - } - - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - try: - async with session.post(f"{api_url}/panel/api/inbounds/updateClient/{client_id}", json=payload, headers=headers) as response: - print(f"POST {response.url} Status: {response.status}") - print(f"POST Request Data: {json.dumps(payload, indent=2)}") + async with session.post( + f"{api_url}/panel/api/inbounds/updateClient/{client_id}", + json=payload, + headers=headers, + ) as response: + logging.info(f"POST {response.url} Status: {response.status}") + logging.info(f"POST Request Data: {json.dumps(payload, indent=2)}") response_text = await response.text() - print(f"POST Response: {response_text}") - + logging.info(f"POST Response: {response_text}") + if response.status == 200: return True else: - print(f"Ошибка при продлении ключа: {response.status} - {response_text}") + logging.error( + f"Ошибка при продлении ключа: {response.status} - {response_text}" + ) return False except Exception as e: - print(f"Ошибка запроса: {e}") + logging.error(f"Ошибка запроса: {e}") return False + async def delete_client(session, server_id: str, client_id: str) -> bool: - api_url = SERVERS[server_id]['API_URL'] + api_url = SERVERS[server_id]["API_URL"] url = f"{api_url}/panel/api/inbounds/1/delClient/{client_id}" - headers = { - 'Accept': 'application/json' - } + headers = {"Accept": "application/json"} try: async with session.post(url, headers=headers) as response: if response.status == 200: return True else: - print(f"Ошибка при удалении клиента: {response.status} - {await response.text()}") + logging.error( + f"Ошибка при удалении клиента: {response.status} - {await response.text()}" + ) return False except Exception as e: - print(f"Ошибка запроса: {e}") + logging.error(f"Ошибка запроса: {e}") return False diff --git a/database.py b/database.py index 912358d2..37fba5b0 100644 --- a/database.py +++ b/database.py @@ -7,16 +7,19 @@ from config import DATABASE_URL async def init_db(): conn = await asyncpg.connect(DATABASE_URL) - - await conn.execute(''' + + 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(''' + """ + ) + + await conn.execute( + """ CREATE TABLE IF NOT EXISTS keys ( tg_id BIGINT NOT NULL, client_id TEXT NOT NULL, @@ -28,182 +31,256 @@ async def init_db(): notified BOOLEAN NOT NULL DEFAULT FALSE, -- новое поле для статуса уведомления PRIMARY KEY (tg_id, client_id) ) - ''') - await conn.execute(''' + """ + ) + await conn.execute( + """ CREATE TABLE IF NOT EXISTS referrals ( referred_tg_id BIGINT PRIMARY KEY NOT NULL, -- ID приглашенного пользователя referrer_tg_id BIGINT NOT NULL, -- ID пригласившего пользователя reward_issued BOOLEAN DEFAULT FALSE -- Был ли начислен бонус ) - ''') - + """ + ) + try: - await conn.execute(''' + await conn.execute( + """ ALTER TABLE keys ADD COLUMN server_id TEXT NOT NULL DEFAULT 'server1' - ''') + """ + ) except asyncpg.exceptions.DuplicateColumnError: pass - + try: - await conn.execute(''' + await conn.execute( + """ ALTER TABLE keys ADD COLUMN notified BOOLEAN NOT NULL DEFAULT FALSE - ''') + """ + ) except asyncpg.exceptions.DuplicateColumnError: pass try: - await conn.execute(''' + await conn.execute( + """ ALTER TABLE keys ADD COLUMN notified_24h BOOLEAN NOT NULL DEFAULT FALSE - ''') + """ + ) except asyncpg.exceptions.DuplicateColumnError: pass await conn.close() + async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0): conn = await asyncpg.connect(DATABASE_URL) - await conn.execute(''' + await conn.execute( + """ INSERT INTO connections (tg_id, balance, trial) VALUES ($1, $2, $3) - ''', tg_id, balance, trial) + """, + tg_id, + balance, + trial, + ) await conn.close() + async def check_connection_exists(tg_id: int): conn = await asyncpg.connect(DATABASE_URL) - exists = await conn.fetchval(''' + exists = await conn.fetchval( + """ SELECT EXISTS(SELECT 1 FROM connections WHERE tg_id = $1) - ''', tg_id) + """, + tg_id, + ) await conn.close() return exists -async def store_key(tg_id: int, client_id: str, email: str, expiry_time: int, key: str, server_id: str): + +async def store_key( + tg_id: int, client_id: str, email: str, expiry_time: int, key: str, server_id: str +): conn = await asyncpg.connect(DATABASE_URL) - await conn.execute(''' + await conn.execute( + """ INSERT INTO keys (tg_id, client_id, email, created_at, expiry_time, key, server_id) VALUES ($1, $2, $3, $4, $5, $6, $7) - ''', tg_id, client_id, email, int(datetime.utcnow().timestamp() * 1000), expiry_time, key, server_id) + """, + tg_id, + client_id, + email, + int(datetime.utcnow().timestamp() * 1000), + expiry_time, + key, + server_id, + ) await conn.close() + async def get_keys(tg_id: int): conn = await asyncpg.connect(DATABASE_URL) - records = await conn.fetch(''' + records = await conn.fetch( + """ SELECT client_id, email, created_at, key FROM keys WHERE tg_id = $1 - ''', tg_id) + """, + tg_id, + ) await conn.close() return records + async def get_keys_by_server(tg_id: int, server_id: str): conn = await asyncpg.connect(DATABASE_URL) - records = await conn.fetch(''' + records = await conn.fetch( + """ SELECT client_id, email, created_at, key FROM keys WHERE tg_id = $1 AND server_id = $2 - ''', tg_id, server_id) + """, + tg_id, + server_id, + ) await conn.close() return records + async def has_active_key(tg_id: int) -> bool: 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_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) + 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): conn = await asyncpg.connect(DATABASE_URL) - await conn.execute(''' + await conn.execute( + """ UPDATE connections SET balance = balance + $1 WHERE tg_id = $2 - ''', amount, tg_id) + """, + amount, + tg_id, + ) await handle_referral_on_balance_update(tg_id, amount) 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) + 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 + async def get_all_users(conn): - return await conn.fetch('SELECT tg_id FROM connections') + return await conn.fetch("SELECT tg_id FROM connections") + async def add_referral(referred_tg_id: int, referrer_tg_id: int): conn = await asyncpg.connect(DATABASE_URL) - await conn.execute(''' + await conn.execute( + """ INSERT INTO referrals (referred_tg_id, referrer_tg_id) VALUES ($1, $2) - ''', referred_tg_id, referrer_tg_id) + """, + referred_tg_id, + referrer_tg_id, + ) await conn.close() + async def handle_referral_on_balance_update(tg_id: int, amount: float): conn = await asyncpg.connect(DATABASE_URL) - referral = await conn.fetchrow(''' + referral = await conn.fetchrow( + """ SELECT referrer_tg_id FROM referrals WHERE referred_tg_id = $1 - ''', tg_id) + """, + tg_id, + ) if referral: - referrer_tg_id = referral['referrer_tg_id'] - - bonus = amount * 0.25 + referrer_tg_id = referral["referrer_tg_id"] + + bonus = amount * 0.25 if bonus < 0: bonus = 0 await update_balance(referrer_tg_id, bonus) - await conn.execute(''' + await conn.execute( + """ UPDATE referrals SET reward_issued = TRUE WHERE referrer_tg_id = $1 AND referred_tg_id = $2 - ''', referrer_tg_id, tg_id) + """, + referrer_tg_id, + tg_id, + ) await conn.close() + async def get_referral_stats(referrer_tg_id: int): conn = await asyncpg.connect(DATABASE_URL) - total_referrals = await conn.fetchval(''' + total_referrals = await conn.fetchval( + """ SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1 - ''', referrer_tg_id) + """, + referrer_tg_id, + ) - active_referrals = await conn.fetchval(''' + active_referrals = await conn.fetchval( + """ SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1 AND reward_issued = TRUE - ''', referrer_tg_id) + """, + referrer_tg_id, + ) await conn.close() - return { - 'total_referrals': total_referrals, - 'active_referrals': active_referrals - } + return {"total_referrals": total_referrals, "active_referrals": active_referrals} + async def update_key_expiry(client_id: str, new_expiry_time: int): """ Обновление времени истечения ключа на новое значение. """ conn = await asyncpg.connect(DATABASE_URL) - await conn.execute(''' + await conn.execute( + """ UPDATE keys SET expiry_time = $1, notified = FALSE, notified_24h = FALSE WHERE client_id = $2 - ''', new_expiry_time, client_id) + """, + new_expiry_time, + client_id, + ) await conn.close() @@ -212,36 +289,51 @@ async def delete_key(client_id: str): Удаление ключа из базы данных. """ conn = await asyncpg.connect(DATABASE_URL) - await conn.execute(''' + await conn.execute( + """ DELETE FROM keys WHERE client_id = $1 - ''', client_id) + """, + client_id, + ) await conn.close() + async def add_balance_to_client(client_id: str, amount: float): conn = await asyncpg.connect(DATABASE_URL) - await conn.execute(''' + await conn.execute( + """ UPDATE connections SET balance = balance + $1 WHERE tg_id = $2 - ''', amount, client_id) + """, + amount, + client_id, + ) await conn.close() + async def get_client_id_by_email(email: str): """ Получение client_id по email. """ conn = await asyncpg.connect(DATABASE_URL) - client_id = await conn.fetchval(''' + client_id = await conn.fetchval( + """ SELECT client_id FROM keys WHERE email = $1 - ''', email) + """, + email, + ) await conn.close() return client_id + async def get_tg_id_by_client_id(client_id: str): conn = await asyncpg.connect(DATABASE_URL) try: - result = await conn.fetchrow('SELECT tg_id FROM keys WHERE client_id = $1', client_id) - return result['tg_id'] if result else None + result = await conn.fetchrow( + "SELECT tg_id FROM keys WHERE client_id = $1", client_id + ) + return result["tg_id"] if result else None finally: - await conn.close() \ No newline at end of file + await conn.close() diff --git a/handlers/admin/admin.py b/handlers/admin/admin.py index 207dfcbf..04dd72dd 100644 --- a/handlers/admin/admin.py +++ b/handlers/admin/admin.py @@ -1,3 +1,4 @@ +import logging from datetime import datetime import asyncpg @@ -6,19 +7,24 @@ from aiogram.filters import Command from auth import login_with_credentials from client import extend_client_key_admin -from config import ADMIN_ID, ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL -from database import (add_balance_to_client, check_connection_exists, - get_client_id_by_email, get_tg_id_by_client_id, - update_key_expiry) +from config import ADMIN_IDS, ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL +from database import ( + add_balance_to_client, + check_connection_exists, + get_client_id_by_email, + get_tg_id_by_client_id, + update_key_expiry, +) +from middlewares.admin import admin_only + +logging.basicConfig(level=logging.DEBUG) router = Router() -@router.message(Command('add_balance')) + +@router.message(Command("add_balance")) +@admin_only() async def cmd_add_balance(message: types.Message): - if message.from_user.id != ADMIN_ID: - await message.reply("У вас нет доступа к этой команде.") - return - try: _, client_id, amount = message.text.split() amount = float(amount) @@ -30,24 +36,28 @@ async def cmd_add_balance(message: types.Message): await add_balance_to_client(int(client_id), amount) await message.reply(f"Баланс клиента {client_id} увеличен на {amount} у.е.") except ValueError: - await message.reply("Пожалуйста, используйте формат: /add_balance ") + await message.reply( + "Пожалуйста, используйте формат: /add_balance " + ) except Exception as e: await message.reply(f"Произошла ошибка: {e}") -@router.message(Command('update_key_expiry')) + +@router.message(Command("update_key_expiry")) +@admin_only() async def cmd_update_key_expiry(message: types.Message): - if message.from_user.id != ADMIN_ID: - await message.reply("У вас нет доступа к этой команде.") - return - try: parts = message.text.split(maxsplit=2) if len(parts) != 3: - await message.reply("Пожалуйста, используйте формат: /update_key_expiry ") + await message.reply( + "Пожалуйста, используйте формат: /update_key_expiry " + ) return - + _, email, expiry_time_str = parts - expiry_time = int(datetime.strptime(expiry_time_str, '%Y-%m-%d %H:%M:%S').timestamp() * 1000) + expiry_time = int( + datetime.strptime(expiry_time_str, "%Y-%m-%d %H:%M:%S").timestamp() * 1000 + ) client_id = await get_client_id_by_email(email) if client_id is None: @@ -55,32 +65,48 @@ async def cmd_update_key_expiry(message: types.Message): return await update_key_expiry(client_id, expiry_time) - + conn = await asyncpg.connect(DATABASE_URL) try: - record = await conn.fetchrow('SELECT server_id FROM keys WHERE client_id = $1', client_id) + record = await conn.fetchrow( + "SELECT server_id FROM keys WHERE client_id = $1", client_id + ) if not record: await message.reply("Клиент не найден в базе данных.") return - - server_id = record['server_id'] + + server_id = record["server_id"] tg_id = await get_tg_id_by_client_id(client_id) - session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) - - print(f"Попытка обновить панель для server_id: {server_id}, tg_id: {tg_id}, client_id: {client_id}, email: {email}, expiryTime: {expiry_time}") + session = await login_with_credentials( + server_id, ADMIN_USERNAME, ADMIN_PASSWORD + ) - success = await extend_client_key_admin(session, server_id, tg_id, client_id, email, expiry_time) + logging.info( + f"Попытка обновить панель для server_id: {server_id}, tg_id: {tg_id}, client_id: {client_id}, email: {email}, expiryTime: {expiry_time}" + ) - print(f"Статус обновления панели: {'Успешно' if success else 'Не удалось'}") + success = await extend_client_key_admin( + session, server_id, tg_id, client_id, email, expiry_time + ) + + logging.info( + f"Статус обновления панели: {'Успешно' if success else 'Не удалось'}" + ) if success: - await message.reply(f"Время истечения ключа для клиента {client_id} ({email}) обновлено и синхронизировано с панелью.") + await message.reply( + f"Время истечения ключа для клиента {client_id} ({email}) обновлено и синхронизировано с панелью." + ) else: - await message.reply(f"Время истечения ключа для клиента {client_id} ({email}) обновлено, но не удалось синхронизировать с панелью.") - + await message.reply( + f"Время истечения ключа для клиента {client_id} ({email}) обновлено, но не удалось синхронизировать с панелью." + ) + finally: await conn.close() except ValueError: - await message.reply("Пожалуйста, используйте формат: /update_key_expiry ") + await message.reply( + "Пожалуйста, используйте формат: /update_key_expiry " + ) except Exception as e: await message.reply(f"Произошла ошибка: {e}") diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index f33a01ec..d451d4fb 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -6,37 +6,63 @@ from aiogram import Router, types from aiogram.filters import Command from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from aiogram.types import (CallbackQuery, InlineKeyboardButton, - InlineKeyboardMarkup, Message) +from aiogram.types import ( + CallbackQuery, + InlineKeyboardButton, + InlineKeyboardMarkup, + Message, +) from backup import backup_database from bot import bot from config import ADMIN_ID, DATABASE_URL from handlers.commands import send_message_to_all_clients +from middlewares.admin import admin_only router = Router() + class UserEditorState(StatesGroup): waiting_for_tg_id = State() displaying_user_info = State() -@router.message(Command('admin')) -async def handle_admin_command(message: types.Message): - if message.from_user.id != ADMIN_ID: - await bot.send_message(message.chat.id, "У вас нет доступа к этой команде.") - return - keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="Статистика пользователей", callback_data="user_stats")], - [InlineKeyboardButton(text="Редактор пользователей", callback_data="user_editor")], - [InlineKeyboardButton(text="Отправить сообщение всем клиентам", callback_data="send_to_alls")], - [InlineKeyboardButton(text="Создать бэкап", callback_data="backups")], - [InlineKeyboardButton(text="Перезапустить бота", callback_data="restart_bot")] - ]) - await bot.send_message(message.chat.id, "Панель администратора", reply_markup=keyboard) +@router.message(Command("admin")) +@admin_only() +async def handle_admin_command(message: types.Message): + keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="Статистика пользователей", callback_data="user_stats" + ) + ], + [ + InlineKeyboardButton( + text="Редактор пользователей", callback_data="user_editor" + ) + ], + [ + InlineKeyboardButton( + text="Отправить сообщение всем клиентам", + callback_data="send_to_alls", + ) + ], + [InlineKeyboardButton(text="Создать бэкап", callback_data="backups")], + [ + InlineKeyboardButton( + text="Перезапустить бота", callback_data="restart_bot" + ) + ], + ] + ) + await bot.send_message( + message.chat.id, "Панель администратора", reply_markup=keyboard + ) @router.callback_query(lambda c: c.data == "user_stats") +@admin_only() async def user_stats_menu(callback_query: CallbackQuery): conn = await asyncpg.connect(DATABASE_URL) try: @@ -44,7 +70,10 @@ async def user_stats_menu(callback_query: CallbackQuery): total_keys = await conn.fetchval("SELECT COUNT(*) FROM keys") total_referrals = await conn.fetchval("SELECT COUNT(*) FROM referrals") - active_keys = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE expiry_time > $1", int(datetime.utcnow().timestamp() * 1000)) + active_keys = await conn.fetchval( + "SELECT COUNT(*) FROM keys WHERE expiry_time > $1", + int(datetime.utcnow().timestamp() * 1000), + ) expired_keys = total_keys - active_keys stats_message = ( @@ -56,49 +85,84 @@ async def user_stats_menu(callback_query: CallbackQuery): f"• Истекшие ключи: {expired_keys}" ) - back_button = InlineKeyboardButton(text="Назад", callback_data="back_to_admin_menu") - keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [back_button] - ]) + back_button = InlineKeyboardButton( + text="Назад", callback_data="back_to_admin_menu" + ) + keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]]) - await callback_query.message.edit_text(stats_message, reply_markup=keyboard, parse_mode="HTML") + await callback_query.message.edit_text( + stats_message, reply_markup=keyboard, parse_mode="HTML" + ) finally: await conn.close() await callback_query.answer() + @router.callback_query(lambda c: c.data == "send_to_alls") +@admin_only() async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext): await send_message_to_all_clients(callback_query.message, state, from_panel=True) - await callback_query.answer() + await callback_query.answer() + @router.callback_query(lambda c: c.data == "backups") +@admin_only() async def handle_backup(message: Message): await message.answer("Запускаю бэкап базы данных...") await backup_database() await message.answer("Бэкап завершен и отправлен админу.") + @router.callback_query(lambda c: c.data == "restart_bot") +@admin_only() async def handle_restart(callback_query: CallbackQuery): if callback_query.from_user.id == ADMIN_ID: try: - result = subprocess.run(['sudo', 'systemctl', 'restart', 'bot.service'], check=True, capture_output=True, text=True) + result = subprocess.run( + ["sudo", "systemctl", "restart", "bot.service"], + check=True, + capture_output=True, + text=True, + ) await callback_query.message.answer("Бот успешно перезапущен.") except subprocess.CalledProcessError as e: - await callback_query.message.answer(f"Бот будет перезапущен через 30 секунд {e.stderr}") + await callback_query.message.answer( + f"Бот будет перезапущен через 30 секунд {e.stderr}" + ) else: - await callback_query.answer("У вас нет доступа к этой команде.", show_alert=True) + await callback_query.answer( + "У вас нет доступа к этой команде.", show_alert=True + ) + @router.callback_query(lambda c: c.data == "user_editor") +@admin_only() async def user_editor_menu(callback_query: CallbackQuery): - keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="Поиск по имени ключа", callback_data="search_by_key_name")], - [InlineKeyboardButton(text="Поиск по tg_id", callback_data="search_by_tg_id")], - [InlineKeyboardButton(text="Назад", callback_data="back_to_admin_menu")] # Back button - ]) - await callback_query.message.edit_text("Выберите метод поиска:", reply_markup=keyboard) + keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="Поиск по имени ключа", callback_data="search_by_key_name" + ) + ], + [ + InlineKeyboardButton( + text="Поиск по tg_id", callback_data="search_by_tg_id" + ) + ], + [ + InlineKeyboardButton(text="Назад", callback_data="back_to_admin_menu") + ], # Back button + ] + ) + await callback_query.message.edit_text( + "Выберите метод поиска:", reply_markup=keyboard + ) + @router.callback_query(lambda c: c.data == "back_to_admin_menu") +@admin_only() async def back_to_admin_menu(callback_query: CallbackQuery): try: await callback_query.message.delete() @@ -107,16 +171,38 @@ async def back_to_admin_menu(callback_query: CallbackQuery): tg_id = callback_query.from_user.id if tg_id == ADMIN_ID: - keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text="Статистика пользователей", callback_data="user_stats")], - [InlineKeyboardButton(text="Редактор пользователей", callback_data="user_editor")], - [InlineKeyboardButton(text="Отправить сообщение всем клиентам", callback_data="send_to_alls")], - [InlineKeyboardButton(text="Создать бэкап", callback_data="backups")], - [InlineKeyboardButton(text="Перезапустить бота", callback_data="restart_bot")] - ]) + keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="Статистика пользователей", callback_data="user_stats" + ) + ], + [ + InlineKeyboardButton( + text="Редактор пользователей", callback_data="user_editor" + ) + ], + [ + InlineKeyboardButton( + text="Отправить сообщение всем клиентам", + callback_data="send_to_alls", + ) + ], + [InlineKeyboardButton(text="Создать бэкап", callback_data="backups")], + [ + InlineKeyboardButton( + text="Перезапустить бота", callback_data="restart_bot" + ) + ], + ] + ) await bot.send_message(tg_id, "Панель администратора", reply_markup=keyboard) else: await bot.send_message(tg_id, "У вас нет доступа к этой команде.") + 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) + await bot.edit_message_text( + message, chat_id=tg_id, message_id=callback_query.message.message_id + ) diff --git a/handlers/admin/user_editor.py b/handlers/admin/user_editor.py index 320f1a4c..321eab5e 100644 --- a/handlers/admin/user_editor.py +++ b/handlers/admin/user_editor.py @@ -1,50 +1,56 @@ -from datetime import datetime - import asyncio import logging +from datetime import datetime + import asyncpg from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from aiogram.types import (CallbackQuery, InlineKeyboardButton, - InlineKeyboardMarkup) +from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup from auth import login_with_credentials from bot import bot from client import delete_client, extend_client_key_admin from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, SERVERS -from database import (get_client_id_by_email, get_tg_id_by_client_id, - update_key_expiry) +from database import get_client_id_by_email, get_tg_id_by_client_id, update_key_expiry from handlers.admin.admin_panel import back_to_admin_menu from handlers.utils import sanitize_key_name - -logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') +logging.basicConfig( + level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s" +) logger = logging.getLogger(__name__) router = Router() + class UserEditorState(StatesGroup): waiting_for_tg_id = State() displaying_user_info = State() - waiting_for_new_balance = State() + waiting_for_new_balance = State() waiting_for_key_name = State() - waiting_for_expiry_time = State() + waiting_for_expiry_time = State() + @router.callback_query(lambda c: c.data == "search_by_tg_id") async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext): await callback_query.message.edit_text("Введите tg_id клиента:") await state.set_state(UserEditorState.waiting_for_tg_id) + @router.message(UserEditorState.waiting_for_tg_id, F.text.isdigit()) async def handle_tg_id_input(message: types.Message, state: FSMContext): tg_id = int(message.text) conn = await asyncpg.connect(DATABASE_URL) try: - balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) + balance = await conn.fetchval( + "SELECT balance FROM connections WHERE tg_id = $1", tg_id + ) key_records = await conn.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) - referral_count = await conn.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) + referral_count = await conn.fetchval( + "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id + ) if balance is None: await message.reply("Пользователь с указанным tg_id не найден.") @@ -55,11 +61,22 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext): [InlineKeyboardButton(text=email, callback_data=f"edit_key_{email}")] for email, in key_records ] - keyboard = InlineKeyboardMarkup(inline_keyboard=[ - *key_buttons, - [InlineKeyboardButton(text="📝 Изменить баланс", callback_data=f"change_balance_{tg_id}")], - [InlineKeyboardButton(text="Назад", callback_data="back_to_user_editor")] - ]) + keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + *key_buttons, + [ + InlineKeyboardButton( + text="📝 Изменить баланс", + callback_data=f"change_balance_{tg_id}", + ) + ], + [ + InlineKeyboardButton( + text="Назад", callback_data="back_to_user_editor" + ) + ], + ] + ) user_info = ( f"Информация о пользователе:\n" @@ -73,32 +90,40 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext): finally: await conn.close() -@router.callback_query(lambda c: c.data.startswith('change_balance_')) + +@router.callback_query(lambda c: c.data.startswith("change_balance_")) async def process_balance_change(callback_query: CallbackQuery, state: FSMContext): - tg_id = int(callback_query.data.split('_')[2]) - await state.update_data(tg_id=tg_id) + tg_id = int(callback_query.data.split("_")[2]) + await state.update_data(tg_id=tg_id) await callback_query.message.edit_text("Введите новую сумму баланса:") await callback_query.answer() - await state.set_state(UserEditorState.waiting_for_new_balance) + await state.set_state(UserEditorState.waiting_for_new_balance) + @router.message(UserEditorState.waiting_for_new_balance) async def handle_new_balance_input(message: types.Message, state: FSMContext): if not message.text.isdigit() or int(message.text) < 0: - await message.reply("Пожалуйста, введите корректную сумму для изменения баланса.") + await message.reply( + "Пожалуйста, введите корректную сумму для изменения баланса." + ) return new_balance = int(message.text) user_data = await state.get_data() - tg_id = user_data.get('tg_id') + tg_id = user_data.get("tg_id") conn = await asyncpg.connect(DATABASE_URL) try: - await conn.execute("UPDATE connections SET balance = $1 WHERE tg_id = $2", new_balance, tg_id) + await conn.execute( + "UPDATE connections SET balance = $1 WHERE tg_id = $2", new_balance, tg_id + ) response_message = f"Баланс успешно изменен на {new_balance}." - - back_button = InlineKeyboardButton(text="Назад в меню админа", callback_data="back_to_user_editor") + + back_button = InlineKeyboardButton( + text="Назад в меню админа", callback_data="back_to_user_editor" + ) keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]]) await message.reply(response_message, reply_markup=keyboard, parse_mode="HTML") @@ -106,27 +131,32 @@ async def handle_new_balance_input(message: types.Message, state: FSMContext): finally: await conn.close() - await state.clear() - + await state.clear() -@router.callback_query(lambda c: c.data.startswith('edit_key_')) + +@router.callback_query(lambda c: c.data.startswith("edit_key_")) async def process_key_edit(callback_query: CallbackQuery): - email = callback_query.data.split('_', 2)[2] + email = callback_query.data.split("_", 2)[2] try: conn = await asyncpg.connect(DATABASE_URL) try: - record = await conn.fetchrow(''' + record = await conn.fetchrow( + """ SELECT k.key, k.expiry_time, k.server_id FROM keys k WHERE k.email = $1 - ''', email) + """, + email, + ) if record: - key = record['key'] - expiry_time = record['expiry_time'] - server_id = record['server_id'] - server_name = SERVERS.get(server_id, {}).get('name', 'Неизвестный сервер') + key = record["key"] + expiry_time = record["expiry_time"] + server_id = record["server_id"] + server_name = SERVERS.get(server_id, {}).get( + "name", "Неизвестный сервер" + ) expiry_date = datetime.utcfromtimestamp(expiry_time / 1000) current_date = datetime.utcnow() @@ -140,7 +170,7 @@ async def process_key_edit(callback_query: CallbackQuery): hours_left = time_left.seconds // 3600 days_left_message = f"Осталось часов: {hours_left}" - formatted_expiry_date = expiry_date.strftime('%d %B %Y года') + formatted_expiry_date = expiry_date.strftime("%d %B %Y года") response_message = ( f"Ключ:
{key}
\n" @@ -149,45 +179,67 @@ async def process_key_edit(callback_query: CallbackQuery): f"Сервер: {server_name}" ) - change_expiry_button = types.InlineKeyboardButton(text='⏳ Изменить время истечения', callback_data=f'change_expiry|{email}') - delete_button = types.InlineKeyboardButton(text='❌ Удалить ключ', callback_data=f'delete_key_admin|{email}') + change_expiry_button = types.InlineKeyboardButton( + text="⏳ Изменить время истечения", + callback_data=f"change_expiry|{email}", + ) + delete_button = types.InlineKeyboardButton( + text="❌ Удалить ключ", callback_data=f"delete_key_admin|{email}" + ) keyboard = types.InlineKeyboardMarkup( inline_keyboard=[ [change_expiry_button, delete_button], - [InlineKeyboardButton(text="Назад", callback_data="back_to_user_editor")] # Кнопка "Назад" + [ + InlineKeyboardButton( + text="Назад", callback_data="back_to_user_editor" + ) + ], # Кнопка "Назад" ] ) - await callback_query.message.edit_text(response_message, reply_markup=keyboard, parse_mode="HTML") + await callback_query.message.edit_text( + response_message, reply_markup=keyboard, parse_mode="HTML" + ) else: - await callback_query.message.edit_text("Информация о ключе не найдена.", parse_mode="HTML") + await callback_query.message.edit_text( + "Информация о ключе не найдена.", parse_mode="HTML" + ) finally: await conn.close() except Exception as e: - await handle_error(callback_query.from_user.id, callback_query, f"Ошибка при получении информации о ключе: {e}") + await handle_error( + callback_query.from_user.id, + callback_query, + f"Ошибка при получении информации о ключе: {e}", + ) await callback_query.answer() + @router.callback_query(lambda c: c.data == "search_by_key_name") async def prompt_key_name(callback_query: CallbackQuery, state: FSMContext): await callback_query.message.edit_text("Введите имя ключа:") await state.set_state(UserEditorState.waiting_for_key_name) + @router.message(UserEditorState.waiting_for_key_name) async def handle_key_name_input(message: types.Message, state: FSMContext): key_name = sanitize_key_name(message.text) conn = await asyncpg.connect(DATABASE_URL) try: - user_records = await conn.fetch(''' + user_records = await conn.fetch( + """ SELECT c.tg_id, c.balance, k.email, k.key, k.expiry_time, k.server_id FROM connections c JOIN keys k ON c.tg_id = k.tg_id WHERE k.email = $1 - ''', key_name) + """, + key_name, + ) if not user_records: await message.reply("Пользователь с указанным именем ключа не найден.") @@ -198,15 +250,17 @@ async def handle_key_name_input(message: types.Message, state: FSMContext): key_buttons = [] for record in user_records: - tg_id = record['tg_id'] - balance = record['balance'] - email = record['email'] - key = record['key'] - expiry_time = record['expiry_time'] - server_id = record['server_id'] - server_name = SERVERS.get(server_id, {}).get('name', 'Неизвестный сервер') + tg_id = record["tg_id"] + balance = record["balance"] + email = record["email"] + key = record["key"] + expiry_time = record["expiry_time"] + server_id = record["server_id"] + server_name = SERVERS.get(server_id, {}).get("name", "Неизвестный сервер") - expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime('%d %B %Y') + expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime( + "%d %B %Y" + ) response_messages.append( f"Ключ:
{key}
\n" @@ -215,36 +269,47 @@ async def handle_key_name_input(message: types.Message, state: FSMContext): f"Сервер: {server_name}" ) - change_expiry_button = InlineKeyboardButton(text='⏳ Изменить время истечения', callback_data=f'change_expiry|{email}') - delete_button = InlineKeyboardButton(text='❌ Удалить ключ', callback_data=f'delete_key_admin|{email}') + change_expiry_button = InlineKeyboardButton( + text="⏳ Изменить время истечения", + callback_data=f"change_expiry|{email}", + ) + delete_button = InlineKeyboardButton( + text="❌ Удалить ключ", callback_data=f"delete_key_admin|{email}" + ) key_buttons.append([change_expiry_button, delete_button]) - key_buttons.append([InlineKeyboardButton(text="Назад", callback_data="back_to_user_editor")]) + key_buttons.append( + [InlineKeyboardButton(text="Назад", callback_data="back_to_user_editor")] + ) keyboard = InlineKeyboardMarkup(inline_keyboard=key_buttons) - await message.reply("\n".join(response_messages), reply_markup=keyboard, parse_mode="HTML") + await message.reply( + "\n".join(response_messages), reply_markup=keyboard, parse_mode="HTML" + ) finally: await conn.close() await state.clear() -@router.callback_query(lambda c: c.data.startswith('change_expiry|')) + +@router.callback_query(lambda c: c.data.startswith("change_expiry|")) async def prompt_expiry_change(callback_query: CallbackQuery, state: FSMContext): - email = callback_query.data.split('|')[1] + email = callback_query.data.split("|")[1] await callback_query.message.edit_text( - f"Введите новое время истечения для ключа {email} в формате YYYY-MM-DD HH:MM:SS:", - parse_mode="HTML" -) + f"Введите новое время истечения для ключа {email} в формате YYYY-MM-DD HH:MM:SS:", + parse_mode="HTML", + ) await state.update_data(email=email) - await state.set_state(UserEditorState.waiting_for_expiry_time) + await state.set_state(UserEditorState.waiting_for_expiry_time) + @router.message(UserEditorState.waiting_for_expiry_time) async def handle_expiry_time_input(message: types.Message, state: FSMContext): user_data = await state.get_data() - email = user_data.get('email') + email = user_data.get("email") if not email: await message.reply("Email не найден в состоянии.") @@ -253,9 +318,11 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext): try: expiry_time_str = message.text - expiry_time = int(datetime.strptime(expiry_time_str, '%Y-%m-%d %H:%M:%S').timestamp() * 1000) + expiry_time = int( + datetime.strptime(expiry_time_str, "%Y-%m-%d %H:%M:%S").timestamp() * 1000 + ) - client_id = await get_client_id_by_email(email) + client_id = await get_client_id_by_email(email) if client_id is None: await message.reply(f"Клиент с email {email} не найден.") await state.clear() @@ -263,7 +330,9 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext): conn = await asyncpg.connect(DATABASE_URL) try: - record = await conn.fetchrow('SELECT server_id FROM keys WHERE client_id = $1', client_id) + record = await conn.fetchrow( + "SELECT server_id FROM keys WHERE client_id = $1", client_id + ) if not record: await message.reply("Клиент не найден в базе данных.") await state.clear() @@ -274,23 +343,29 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext): async def update_key_on_all_servers(): tasks = [] for server_id in SERVERS: - tasks.append(asyncio.create_task( - renew_server_key(server_id, tg_id, client_id, email, expiry_time) - )) + tasks.append( + asyncio.create_task( + renew_server_key( + server_id, tg_id, client_id, email, expiry_time + ) + ) + ) await asyncio.gather(*tasks) await update_key_on_all_servers() await update_key_expiry(client_id, expiry_time) - response_message = ( - f"Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах." - ) + response_message = f"Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах." - back_button = InlineKeyboardButton(text="Назад", callback_data="back_to_user_editor") + back_button = InlineKeyboardButton( + text="Назад", callback_data="back_to_user_editor" + ) keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]]) - await message.reply(response_message, reply_markup=keyboard, parse_mode="HTML") + await message.reply( + response_message, reply_markup=keyboard, parse_mode="HTML" + ) finally: await conn.close() @@ -302,51 +377,87 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext): await state.clear() + async def renew_server_key(server_id, tg_id, client_id, email, new_expiry_time): try: - session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) - await extend_client_key_admin(session, server_id, tg_id, client_id, email, new_expiry_time) + session = await login_with_credentials( + server_id, ADMIN_USERNAME, ADMIN_PASSWORD + ) + await extend_client_key_admin( + session, server_id, tg_id, client_id, email, new_expiry_time + ) except Exception as e: - logger.error(f"Не удалось обновить ключ {client_id} на сервере {server_id}: {e}") + logger.error( + f"Не удалось обновить ключ {client_id} на сервере {server_id}: {e}" + ) -@router.callback_query(lambda c: c.data.startswith('delete_key_admin|')) + +@router.callback_query(lambda c: c.data.startswith("delete_key_admin|")) async def process_callback_delete_key(callback_query: types.CallbackQuery): tg_id = callback_query.from_user.id - email = callback_query.data.split('|')[1] + email = callback_query.data.split("|")[1] conn = await asyncpg.connect(DATABASE_URL) try: - client_id = await conn.fetchval('SELECT client_id FROM keys WHERE email = $1', email) + client_id = await conn.fetchval( + "SELECT client_id FROM keys WHERE email = $1", email + ) if client_id is None: - await bot.edit_message_text("Ключ не найден.", chat_id=tg_id, message_id=callback_query.message.message_id) + await bot.edit_message_text( + "Ключ не найден.", + chat_id=tg_id, + message_id=callback_query.message.message_id, + ) return - confirmation_keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton(text='✅ Да, удалить', callback_data=f'confirm_delete_admin|{client_id}')], - [types.InlineKeyboardButton(text='❌ Нет, отменить', callback_data='view_keys')] - ]) + confirmation_keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text="✅ Да, удалить", + callback_data=f"confirm_delete_admin|{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, parse_mode="HTML") + await bot.edit_message_text( + "Вы уверены, что хотите удалить ключ?", + chat_id=tg_id, + message_id=callback_query.message.message_id, + reply_markup=confirmation_keyboard, + parse_mode="HTML", + ) finally: await conn.close() await callback_query.answer() -@router.callback_query(lambda c: c.data.startswith('confirm_delete_admin|')) + +@router.callback_query(lambda c: c.data.startswith("confirm_delete_admin|")) async def process_callback_confirm_delete(callback_query: types.CallbackQuery): tg_id = callback_query.from_user.id - client_id = callback_query.data.split('|')[1] + client_id = callback_query.data.split("|")[1] try: conn = await asyncpg.connect(DATABASE_URL) try: - record = await conn.fetchrow('SELECT email FROM keys WHERE client_id = $1', client_id) + record = await conn.fetchrow( + "SELECT email FROM keys WHERE client_id = $1", client_id + ) if record: - email = record['email'] + email = record["email"] response_message = "Ключ успешно удален." - back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys') + back_button = types.InlineKeyboardButton( + text="Назад", callback_data="view_keys" + ) keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) async def delete_key_from_servers(): @@ -355,50 +466,78 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery): tasks.append(delete_key_from_server(server_id, client_id)) await asyncio.gather(*tasks) - await delete_key_from_servers() - await delete_key_from_db(client_id) + await delete_key_from_servers() + await delete_key_from_db(client_id) - await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard) + await bot.edit_message_text( + response_message, + chat_id=tg_id, + message_id=callback_query.message.message_id, + reply_markup=keyboard, + ) else: response_message = "Ключ не найден или уже удален." - back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys') + 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) + 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 bot.edit_message_text( + f"Ошибка при удалении ключа: {e}", + chat_id=tg_id, + message_id=callback_query.message.message_id, + ) await callback_query.answer() + async def delete_key_from_server(server_id, client_id): """Удаление ключа с сервера""" try: - session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + session = await login_with_credentials( + server_id, ADMIN_USERNAME, ADMIN_PASSWORD + ) success = await delete_client(session, server_id, client_id) if not success: logger.error(f"Ошибка удаления ключа {client_id} на сервере {server_id}") except Exception as e: - logger.error(f"Ошибка при удалении ключа {client_id} с сервера {server_id}: {e}") + logger.error( + f"Ошибка при удалении ключа {client_id} с сервера {server_id}: {e}" + ) + async def delete_key_from_db(client_id): """Удаление ключа из базы данных""" try: conn = await asyncpg.connect(DATABASE_URL) - await conn.execute('DELETE FROM keys WHERE client_id = $1', client_id) + await conn.execute("DELETE FROM keys WHERE client_id = $1", client_id) except Exception as e: logger.error(f"Ошибка при удалении ключа {client_id} из базы данных: {e}") finally: await conn.close() + @router.callback_query(lambda c: c.data == "back_to_user_editor") async def back_to_user_editor(callback_query: CallbackQuery): await back_to_admin_menu(callback_query) -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, parse_mode="HTML") +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, + parse_mode="HTML", + ) diff --git a/handlers/commands.py b/handlers/commands.py index e34a4c1e..efcd43e7 100644 --- a/handlers/commands.py +++ b/handlers/commands.py @@ -1,3 +1,5 @@ +import logging + import asyncpg from aiogram import F, Router, types from aiogram.filters import Command @@ -9,44 +11,55 @@ from bot import bot from config import ADMIN_ID, DATABASE_URL from handlers.admin.admin import cmd_add_balance from handlers.keys.key_management import handle_key_name_input -from handlers.payment.pay import (ReplenishBalanceState, - process_custom_amount_input) +from handlers.payment.yookassa_pay import ( + ReplenishBalanceState, + process_custom_amount_input, +) from handlers.profile import process_callback_view_profile from handlers.start import start_command from handlers.texts import TRIAL +logging.basicConfig(level=logging.DEBUG) + router = Router() + class Form(StatesGroup): waiting_for_server_selection = State() waiting_for_key_name = State() viewing_profile = State() waiting_for_message = State() -@router.message(Command('backup')) + +@router.message(Command("backup")) async def backup_command(message: Message): if message.from_user.id != ADMIN_ID: await message.answer("У вас нет прав для выполнения этой команды.") return from backup import backup_database + await message.answer("Запускаю бэкап базы данных...") await backup_database() await message.answer("Бэкап завершен и отправлен админу.") -@router.message(Command('start')) + +@router.message(Command("start")) async def handle_start(message: types.Message, state: FSMContext): await start_command(message) -@router.message(Command('add_balance')) + +@router.message(Command("add_balance")) async def handle_add_balance(message: types.Message, state: FSMContext): await cmd_add_balance(message) -@router.message(Command('menu')) + +@router.message(Command("menu")) async def handle_menu(message: types.Message, state: FSMContext): await start_command(message) -@router.message(Command('send_trial')) + +@router.message(Command("send_trial")) async def handle_send_trial_command(message: types.Message, state: FSMContext): # Проверка на администратора if message.from_user.id != ADMIN_ID: @@ -56,25 +69,35 @@ async def handle_send_trial_command(message: types.Message, state: FSMContext): try: conn = await asyncpg.connect(DATABASE_URL) try: - records = await conn.fetch(''' + records = await conn.fetch( + """ SELECT tg_id FROM connections WHERE trial = 0 - ''') + """ + ) if records: for record in records: - tg_id = record['tg_id'] + tg_id = record["tg_id"] trial_message = TRIAL try: await bot.send_message(chat_id=tg_id, text=trial_message) except Exception as e: if "Forbidden: bot was blocked by the user" in str(e): - print(f"Бот заблокирован пользователем с tg_id: {tg_id}") + logging.info( + f"Бот заблокирован пользователем с tg_id: {tg_id}" + ) else: - print(f"Ошибка при отправке сообщения пользователю {tg_id}: {e}") + logging.error( + f"Ошибка при отправке сообщения пользователю {tg_id}: {e}" + ) - await message.answer("Сообщения о пробном периоде отправлены всем пользователям с не использованным ключом.") + await message.answer( + "Сообщения о пробном периоде отправлены всем пользователям с не использованным ключом." + ) else: - await message.answer("Нет пользователей с не использованными пробными ключами.") + await message.answer( + "Нет пользователей с не использованными пробными ключами." + ) finally: await conn.close() @@ -82,54 +105,63 @@ async def handle_send_trial_command(message: types.Message, state: FSMContext): except Exception as e: await message.answer(f"Ошибка при отправке сообщений: {e}") -@router.message(Command('send_to_all')) -async def send_message_to_all_clients(message: types.Message, state: FSMContext, from_panel=False): + +@router.message(Command("send_to_all")) +async def send_message_to_all_clients( + message: types.Message, state: FSMContext, from_panel=False +): if not from_panel and message.from_user.id != ADMIN_ID: await message.answer("У вас нет прав для выполнения этой команды.") return - await message.answer("Введите текст сообщения, который вы хотите отправить всем клиентам:") - await state.set_state(Form.waiting_for_message) + await message.answer( + "Введите текст сообщения, который вы хотите отправить всем клиентам:" + ) + await state.set_state(Form.waiting_for_message) + @router.message(Form.waiting_for_message) async def process_message_to_all(message: types.Message, state: FSMContext): - text_message = message.text + text_message = message.text try: conn = await asyncpg.connect(DATABASE_URL) - tg_ids = await conn.fetch('SELECT tg_id FROM connections') + tg_ids = await conn.fetch("SELECT tg_id FROM connections") for record in tg_ids: - tg_id = record['tg_id'] + tg_id = record["tg_id"] try: await bot.send_message(chat_id=tg_id, text=text_message) except Exception as e: - print(f"Ошибка при отправке сообщения пользователю {tg_id}: {e}. Пропускаем этого пользователя.") + logging.error( + f"Ошибка при отправке сообщения пользователю {tg_id}: {e}. Пропускаем этого пользователя." + ) await message.answer("Сообщение было отправлено всем клиентам.") except Exception as e: - print(f"Ошибка при подключении к базе данных: {e}") + logging.error(f"Ошибка при подключении к базе данных: {e}") await message.answer("Произошла ошибка при отправке сообщения.") finally: await conn.close() await state.clear() + @router.message() async def handle_text(message: types.Message, state: FSMContext): current_state = await state.get_state() if message.text in ["/send_to_all"]: - await send_message_to_all_clients(message, state) + await send_message_to_all_clients(message, state) return if message.text == "Мой профиль": callback_query = types.CallbackQuery( id="1", from_user=message.from_user, - chat_instance='', - data='view_profile', - message=message + chat_instance="", + data="view_profile", + message=message, ) await process_callback_view_profile(callback_query, state) return @@ -146,5 +178,5 @@ async def handle_text(message: types.Message, state: FSMContext): await backup_command(message) return - elif current_state is None: + elif current_state is None: await start_command(message) diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index 3c073f60..87394869 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -1,8 +1,7 @@ import os from aiogram import types -from aiogram.types import (BufferedInputFile, InlineKeyboardButton, - InlineKeyboardMarkup) +from aiogram.types import BufferedInputFile, InlineKeyboardButton, InlineKeyboardMarkup from handlers.texts import INSTRUCTIONS @@ -10,26 +9,24 @@ from handlers.texts import INSTRUCTIONS async def send_instructions(callback_query: types.CallbackQuery): await callback_query.message.delete() - instructions_message = ( - INSTRUCTIONS - ) + instructions_message = INSTRUCTIONS - image_path = os.path.join(os.path.dirname(__file__), 'instructions.jpg') + image_path = os.path.join(os.path.dirname(__file__), "instructions.jpg") if not os.path.isfile(image_path): await callback_query.message.answer("Файл изображения не найден.") await callback_query.answer() return - back_button = InlineKeyboardButton(text='🔙 Назад', callback_data='back_to_main') + back_button = InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main") keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]]) - with open(image_path, 'rb') as image_from_buffer: + with open(image_path, "rb") as image_from_buffer: await callback_query.message.answer_photo( BufferedInputFile(image_from_buffer.read(), filename="instructions.jpg"), caption=instructions_message, - parse_mode='Markdown', - reply_markup=keyboard + parse_mode="Markdown", + reply_markup=keyboard, ) - + await callback_query.answer() diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index acb46fe7..f3563035 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -7,14 +7,24 @@ import asyncpg from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from aiogram.types import (CallbackQuery, InlineKeyboardButton, - InlineKeyboardMarkup, Message) +from aiogram.types import ( + CallbackQuery, + InlineKeyboardButton, + InlineKeyboardMarkup, + Message, +) from auth import login_with_credentials from bot import bot, dp from client import add_client -from config import (ADMIN_PASSWORD, ADMIN_USERNAME, APP_URL, DATABASE_URL, - PUBLIC_LINK, SERVERS) +from config import ( + ADMIN_PASSWORD, + ADMIN_USERNAME, + APP_URL, + DATABASE_URL, + PUBLIC_LINK, + SERVERS, +) from database import add_connection, get_balance, store_key, update_balance from handlers.instructions.instructions import send_instructions from handlers.profile import process_callback_view_profile @@ -23,23 +33,29 @@ from handlers.utils import sanitize_key_name router = Router() -logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') +logging.basicConfig( + level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s" +) logger = logging.getLogger(__name__) + class Form(StatesGroup): waiting_for_server_selection = State() waiting_for_key_name = State() viewing_profile = State() waiting_for_message = State() -@dp.callback_query(F.data == 'create_key') + +@dp.callback_query(F.data == "create_key") async def process_callback_create_key(callback_query: CallbackQuery, state: FSMContext): tg_id = callback_query.from_user.id try: - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) + await bot.delete_message( + chat_id=tg_id, message_id=callback_query.message.message_id + ) except Exception: - pass + pass server_id = "все сервера" await state.update_data(selected_server_id=server_id) @@ -47,81 +63,102 @@ async def process_callback_create_key(callback_query: CallbackQuery, state: FSMC await callback_query.answer() - async def select_server(callback_query: CallbackQuery, state: FSMContext): selected_server_id = (await state.get_data()).get("selected_server_id") conn = await asyncpg.connect(DATABASE_URL) try: - existing_connection = await conn.fetchrow('SELECT trial FROM connections WHERE tg_id = $1', callback_query.from_user.id) + existing_connection = await conn.fetchrow( + "SELECT trial FROM connections WHERE tg_id = $1", + callback_query.from_user.id, + ) finally: await conn.close() - trial_status = existing_connection['trial'] if existing_connection else 0 + trial_status = existing_connection["trial"] if existing_connection else 0 if trial_status == 1: await bot.send_message( chat_id=callback_query.from_user.id, text=KEY, parse_mode="HTML", - reply_markup=InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='✅ Да, подключить новое устройство', callback_data='confirm_create_new_key')], - [InlineKeyboardButton(text='↩️ Назад', callback_data='cancel_create_key')] - ]) + 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 bot.send_message( - chat_id=callback_query.from_user.id, - text=KEY_TRIAL, - parse_mode="HTML" + chat_id=callback_query.from_user.id, text=KEY_TRIAL, parse_mode="HTML" ) await state.set_state(Form.waiting_for_key_name) await callback_query.answer() -@dp.callback_query(F.data == 'confirm_create_new_key') +@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 data = await state.get_data() - server_id = data.get('selected_server_id') + server_id = data.get("selected_server_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( - NULL_BALANCE, - reply_markup=keyboard + replenish_button = InlineKeyboardButton( + text="Перейти в профиль", callback_data="view_profile" ) + keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]]) + await callback_query.message.edit_text(NULL_BALANCE, reply_markup=keyboard) await state.clear() return - await callback_query.message.edit_text("🔑 Пожалуйста, введите имя подключаемого устройства:") + 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') + +@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() + async def handle_key_name_input(message: Message, state: FSMContext): tg_id = message.from_user.id key_name = sanitize_key_name(message.text) if not key_name: - await message.bot.send_message(tg_id, "📝 Пожалуйста, назовите устройство на английском языке.") + await message.bot.send_message( + tg_id, "📝 Пожалуйста, назовите устройство на английском языке." + ) return conn = await asyncpg.connect(DATABASE_URL) try: - existing_key = await conn.fetchrow('SELECT * FROM keys WHERE email = $1', key_name.lower()) + existing_key = await conn.fetchrow( + "SELECT * FROM keys WHERE email = $1", key_name.lower() + ) if existing_key: - await message.bot.send_message(tg_id, "❌ Это имя уже используется. Пожалуйста, выберите другое имя для ключа.") + await message.bot.send_message( + tg_id, + "❌ Это имя уже используется. Пожалуйста, выберите другое имя для ключа.", + ) await state.set_state(Form.waiting_for_key_name) return finally: @@ -135,20 +172,28 @@ async def handle_key_name_input(message: Message, state: FSMContext): conn = await asyncpg.connect(DATABASE_URL) try: - existing_connection = await conn.fetchrow('SELECT trial FROM connections WHERE tg_id = $1', tg_id) + 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 + trial_status = existing_connection["trial"] if existing_connection else 0 if trial_status == 0: expiry_time = current_time + timedelta(days=1, hours=3) else: balance = await get_balance(tg_id) if balance < 100: - replenish_button = InlineKeyboardButton(text='Перейти в профиль', callback_data='view_profile') + 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 message.bot.send_message( + tg_id, + "❗️ Недостаточно средств на балансе для создания подписки на новое устройство.", + reply_markup=keyboard, + ) await state.clear() return @@ -158,67 +203,96 @@ async def handle_key_name_input(message: Message, state: FSMContext): expiry_timestamp = int(expiry_time.timestamp() * 1000) public_link = f"{PUBLIC_LINK}{email}" - button_profile = InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile') + button_profile = InlineKeyboardButton( + text="👤 Мой профиль", callback_data="view_profile" + ) button_iphone = InlineKeyboardButton( - text='🍏 Подключить', - url=f'{APP_URL}/?url=v2raytun://import/{public_link}' + text="🍏 Подключить", url=f"{APP_URL}/?url=v2raytun://import/{public_link}" ) button_android = InlineKeyboardButton( - text='🤖 Подключить', - url=f'{APP_URL}/?url=v2raytun://import-sub?url={public_link}' + text="🤖 Подключить", + url=f"{APP_URL}/?url=v2raytun://import-sub?url={public_link}", ) button_download_ios = InlineKeyboardButton( - text='🍏 Скачать', - url="https://apps.apple.com/ru/app/v2raytun/id6476628951" + text="🍏 Скачать", url="https://apps.apple.com/ru/app/v2raytun/id6476628951" ) button_download_android = InlineKeyboardButton( - text='🤖 Скачать', - url="https://play.google.com/store/apps/details?id=com.v2raytun.android&hl=ru" + text="🤖 Скачать", + url="https://play.google.com/store/apps/details?id=com.v2raytun.android&hl=ru", ) - keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [button_download_ios, button_download_android], - [button_iphone, button_android], - [button_profile] - ]) + keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [button_download_ios, button_download_android], + [button_iphone, button_android], + [button_profile], + ] + ) remaining_time = expiry_time - current_time days = remaining_time.days - key_message = key_message_success(public_link, f"Оставшееся время ключа: {days} день") + key_message = key_message_success( + public_link, f"Оставшееся время ключа: {days} день" + ) - await message.bot.send_message(tg_id, key_message, parse_mode="HTML", reply_markup=keyboard) + await message.bot.send_message( + tg_id, key_message, parse_mode="HTML", reply_markup=keyboard + ) try: tasks = [] for server_id in SERVERS: - tasks.append(asyncio.create_task(create_key_on_server(server_id, tg_id, client_id, email, expiry_timestamp))) + tasks.append( + asyncio.create_task( + create_key_on_server( + server_id, tg_id, client_id, email, expiry_timestamp + ) + ) + ) await asyncio.gather(*tasks) conn = await asyncpg.connect(DATABASE_URL) try: - existing_connection = await conn.fetchrow('SELECT * FROM connections WHERE tg_id = $1', tg_id) + 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) + 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_timestamp, public_link, 'all_servers') + await store_key( + tg_id, client_id, email, expiry_timestamp, public_link, "all_servers" + ) except Exception as e: await message.bot.send_message(tg_id, f"❌ Ошибка при создании ключа: {e}") await state.clear() + async def create_key_on_server(server_id, tg_id, client_id, email, expiry_timestamp): try: - session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + session = await login_with_credentials( + server_id, ADMIN_USERNAME, ADMIN_PASSWORD + ) response = await add_client( - session, server_id, client_id, email, tg_id, limit_ip=1, total_gb=0, - expiry_time=expiry_timestamp, enable=True, flow="xtls-rprx-vision" + session, + server_id, + client_id, + email, + tg_id, + limit_ip=1, + total_gb=0, + expiry_time=expiry_timestamp, + enable=True, + flow="xtls-rprx-vision", ) if not response.get("success", True): error_msg = response.get("msg", "Неизвестная ошибка.") @@ -230,11 +304,12 @@ async def create_key_on_server(server_id, tg_id, client_id, email, expiry_timest logger.error(f"Ошибка на сервере {server_id}: {e}") -@dp.callback_query(F.data == 'instructions') +@dp.callback_query(F.data == "instructions") async def handle_instructions(callback_query: CallbackQuery): - await send_instructions(callback_query) + await send_instructions(callback_query) -@dp.callback_query(F.data == 'back_to_main') + +@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() diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 1351b2f5..8ae8ee73 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -11,41 +11,69 @@ from aiogram.types import BufferedInputFile from auth import login_with_credentials from bot import bot from client import add_client, delete_client, extend_client_key -from config import (ADMIN_PASSWORD, ADMIN_USERNAME, APP_URL, DATABASE_URL, - PUBLIC_LINK, SERVERS) -from database import (delete_key, get_balance, store_key, update_balance, - update_key_expiry) -from handlers.texts import (INSUFFICIENT_FUNDS_MSG, KEY_NOT_FOUND_MSG, NO_KEYS, - PLAN_SELECTION_MSG, RENEWAL_PLANS, - SUCCESS_RENEWAL_MSG, key_message) +from config import ( + ADMIN_PASSWORD, + ADMIN_USERNAME, + APP_URL, + DATABASE_URL, + PUBLIC_LINK, + SERVERS, +) +from database import ( + delete_key, + get_balance, + store_key, + update_balance, + update_key_expiry, +) +from handlers.texts import ( + INSUFFICIENT_FUNDS_MSG, + KEY_NOT_FOUND_MSG, + NO_KEYS, + PLAN_SELECTION_MSG, + RENEWAL_PLANS, + SUCCESS_RENEWAL_MSG, + key_message, +) -locale.setlocale(locale.LC_TIME, 'ru_RU.UTF-8') +locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8") router = Router() -logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') +logging.basicConfig( + level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s" +) logger = logging.getLogger(__name__) -@router.callback_query(lambda c: c.data == 'view_keys') + +@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: conn = await asyncpg.connect(DATABASE_URL) try: - records = await conn.fetch(''' + records = await conn.fetch( + """ SELECT email, client_id FROM keys WHERE tg_id = $1 - ''', tg_id) + """, + tg_id, + ) if records: buttons = [] for record in records: - key_name = record['email'] - client_id = record['client_id'] - button = types.InlineKeyboardButton(text=f"🔑 {key_name}", callback_data=f'view_key|{key_name}|{client_id}') + key_name = record["email"] + client_id = record["client_id"] + button = types.InlineKeyboardButton( + text=f"🔑 {key_name}", + callback_data=f"view_key|{key_name}|{client_id}", + ) buttons.append([button]) - back_button = types.InlineKeyboardButton(text='🔙 Назад', callback_data='view_profile') + back_button = types.InlineKeyboardButton( + text="🔙 Назад", callback_data="view_profile" + ) buttons.append([back_button]) inline_keyboard = types.InlineKeyboardMarkup(inline_keyboard=buttons) @@ -54,28 +82,38 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery): "Нажмите на имя устройства для управления его подпиской." ) - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) + await bot.delete_message( + chat_id=tg_id, message_id=callback_query.message.message_id + ) await bot.send_message( chat_id=tg_id, text=response_message, reply_markup=inline_keyboard, - parse_mode="HTML" + parse_mode="HTML", ) else: - response_message = NO_KEYS - create_key_button = types.InlineKeyboardButton(text='➕ Создать ключ', callback_data='create_key') - back_button = types.InlineKeyboardButton(text='🔙 Назад', callback_data='view_profile') - - keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[create_key_button], [back_button]]) + response_message = NO_KEYS + create_key_button = types.InlineKeyboardButton( + text="➕ Создать ключ", callback_data="create_key" + ) + back_button = types.InlineKeyboardButton( + text="🔙 Назад", callback_data="view_profile" + ) - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[[create_key_button], [back_button]] + ) + + await bot.delete_message( + chat_id=tg_id, message_id=callback_query.message.message_id + ) await bot.send_message( chat_id=tg_id, text=response_message, reply_markup=keyboard, - parse_mode="HTML" + parse_mode="HTML", ) finally: @@ -86,31 +124,41 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery): await callback_query.answer() -@router.callback_query(lambda c: c.data.startswith('view_key|')) + +@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('|')[1], callback_query.data.split('|')[2] + key_name, client_id = ( + callback_query.data.split("|")[1], + callback_query.data.split("|")[2], + ) try: try: - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) + await bot.delete_message( + chat_id=tg_id, message_id=callback_query.message.message_id + ) except Exception: pass conn = await asyncpg.connect(DATABASE_URL) try: - record = await conn.fetchrow(''' + record = await conn.fetchrow( + """ SELECT k.expiry_time, k.server_id, k.key FROM keys k WHERE k.tg_id = $1 AND k.email = $2 - ''', tg_id, key_name) + """, + tg_id, + key_name, + ) if record: - key = record['key'] - expiry_time = record['expiry_time'] - server_id = record['server_id'] + key = record["key"] + expiry_time = record["expiry_time"] + server_id = record["server_id"] - server_name = SERVERS.get(server_id, {}).get('name', 'мультисервер') + server_name = SERVERS.get(server_id, {}).get("name", "мультисервер") expiry_date = datetime.utcfromtimestamp(expiry_time / 1000) current_date = datetime.utcnow() time_left = expiry_date - current_date @@ -123,259 +171,387 @@ async def process_callback_view_key(callback_query: types.CallbackQuery): hours_left = time_left.seconds // 3600 days_left_message = f"Осталось часов: {hours_left}" - formatted_expiry_date = expiry_date.strftime('%d %B %Y года') - response_message = key_message(key, formatted_expiry_date, days_left_message, server_name) + formatted_expiry_date = expiry_date.strftime("%d %B %Y года") + response_message = key_message( + key, formatted_expiry_date, days_left_message, server_name + ) download_android_button = types.InlineKeyboardButton( - text='🤖 Скачать', - url='https://play.google.com/store/apps/details?id=com.v2raytun.android&hl=ru' + text="🤖 Скачать", + url="https://play.google.com/store/apps/details?id=com.v2raytun.android&hl=ru", ) download_iphone_button = types.InlineKeyboardButton( - text='🍏 Скачать', - url='https://apps.apple.com/ru/app/v2raytun/id6476628951' + text="🍏 Скачать", + url="https://apps.apple.com/ru/app/v2raytun/id6476628951", ) connect_iphone_button = types.InlineKeyboardButton( - text='🍏 Подключить', - url=f'{APP_URL}/?url=v2raytun://import/{key}' + text="🍏 Подключить", url=f"{APP_URL}/?url=v2raytun://import/{key}" ) connect_android_button = types.InlineKeyboardButton( - text='🤖 Подключить', - url=f'{APP_URL}/?url=v2raytun://import-sub?url={key}' + text="🤖 Подключить", + url=f"{APP_URL}/?url=v2raytun://import-sub?url={key}", ) - renew_button = types.InlineKeyboardButton(text='⏳ Продлить', callback_data=f'renew_key|{client_id}') - delete_button = types.InlineKeyboardButton(text='❌ Удалить', callback_data=f'delete_key|{client_id}') - back_button = types.InlineKeyboardButton(text='🔙 Назад в профиль', callback_data='view_profile') + renew_button = types.InlineKeyboardButton( + text="⏳ Продлить", callback_data=f"renew_key|{client_id}" + ) + delete_button = types.InlineKeyboardButton( + text="❌ Удалить", callback_data=f"delete_key|{client_id}" + ) + back_button = types.InlineKeyboardButton( + text="🔙 Назад в профиль", callback_data="view_profile" + ) inline_keyboard = [ [download_iphone_button, download_android_button], - [connect_iphone_button, connect_android_button], - [renew_button, delete_button], + [connect_iphone_button, connect_android_button], + [renew_button, delete_button], ] if not key.startswith(PUBLIC_LINK): - update_subscription_button = types.InlineKeyboardButton(text='🔄 Обновить подписку', callback_data=f'update_subscription|{client_id}') - inline_keyboard.append([update_subscription_button]) + update_subscription_button = types.InlineKeyboardButton( + text="🔄 Обновить подписку", + callback_data=f"update_subscription|{client_id}", + ) + inline_keyboard.append([update_subscription_button]) - inline_keyboard.append([back_button]) + inline_keyboard.append([back_button]) keyboard = types.InlineKeyboardMarkup(inline_keyboard=inline_keyboard) - image_path = os.path.join(os.path.dirname(__file__), 'pic_view.jpg') + image_path = os.path.join(os.path.dirname(__file__), "pic_view.jpg") if not os.path.isfile(image_path): await bot.send_message(tg_id, "Файл изображения не найден.") return - with open(image_path, 'rb') as image_file: + with open(image_path, "rb") as image_file: await bot.send_photo( chat_id=tg_id, - photo=BufferedInputFile(image_file.read(), filename="pic_view.jpg"), + photo=BufferedInputFile( + image_file.read(), filename="pic_view.jpg" + ), caption=response_message, reply_markup=keyboard, - parse_mode="HTML" + parse_mode="HTML", ) else: - await bot.send_message(chat_id=tg_id, text="Информация о подписке не найдена.", parse_mode="HTML") + await bot.send_message( + chat_id=tg_id, + text="Информация о подписке не найдена.", + parse_mode="HTML", + ) finally: await conn.close() except Exception as e: - await handle_error(tg_id, callback_query, f"Ошибка при получении информации о ключе: {e}") + await handle_error( + tg_id, callback_query, f"Ошибка при получении информации о ключе: {e}" + ) await callback_query.answer() -@router.callback_query(lambda c: c.data.startswith('update_subscription|')) + +@router.callback_query(lambda c: c.data.startswith("update_subscription|")) async def process_callback_update_subscription(callback_query: types.CallbackQuery): tg_id = callback_query.from_user.id - client_id = callback_query.data.split('|')[1] + client_id = callback_query.data.split("|")[1] try: conn = await asyncpg.connect(DATABASE_URL) try: - record = await conn.fetchrow(''' + record = await conn.fetchrow( + """ SELECT k.key, k.expiry_time, k.email, k.server_id FROM keys k WHERE k.tg_id = $1 AND k.client_id = $2 - ''', tg_id, client_id) + """, + tg_id, + client_id, + ) if record: - expiry_time = record['expiry_time'] - email = record['email'] + expiry_time = record["expiry_time"] + email = record["email"] public_link = f"{PUBLIC_LINK}{email}" try: - await conn.execute(''' + await conn.execute( + """ DELETE FROM keys WHERE tg_id = $1 AND client_id = $2 - ''', tg_id, client_id) + """, + tg_id, + client_id, + ) except Exception as delete_error: - await bot.send_message(tg_id, f"Ошибка при удалении старой подписки: {delete_error}") + await bot.send_message( + tg_id, f"Ошибка при удалении старой подписки: {delete_error}" + ) return tasks = [] for server_id in SERVERS: - tasks.append(update_key_on_server(tg_id, client_id, email, expiry_time, server_id)) + tasks.append( + update_key_on_server( + tg_id, client_id, email, expiry_time, server_id + ) + ) results = await asyncio.gather(*tasks) - await store_key(tg_id, client_id, email, expiry_time, public_link, server_id='все сервера') + await store_key( + tg_id, + client_id, + email, + expiry_time, + public_link, + server_id="все сервера", + ) try: - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) + await bot.delete_message( + chat_id=tg_id, message_id=callback_query.message.message_id + ) except Exception as e: logger.error(f"Ошибка при удалении сообщения: {e}") response_message = f"Ваша подписка {email} обновлена!" - back_button = types.InlineKeyboardButton(text='🔙 Назад в профиль', callback_data='view_profile') + back_button = types.InlineKeyboardButton( + text="🔙 Назад в профиль", callback_data="view_profile" + ) keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) await bot.send_message( - tg_id, - response_message, - reply_markup=keyboard, - parse_mode="HTML" + tg_id, response_message, reply_markup=keyboard, parse_mode="HTML" ) else: try: - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) + await bot.delete_message( + chat_id=tg_id, message_id=callback_query.message.message_id + ) except Exception as e: logger.error(f"Ошибка при удалении сообщения: {e}") await bot.send_message( - tg_id, - "Ключ не найден в базе данных.", - parse_mode="HTML" + tg_id, "Ключ не найден в базе данных.", parse_mode="HTML" ) finally: await conn.close() except Exception as e: - await handle_error(tg_id, callback_query, f"Ошибка при обновлении подписки: {e}") + await handle_error( + tg_id, callback_query, f"Ошибка при обновлении подписки: {e}" + ) await callback_query.answer() + async def update_key_on_server(tg_id, client_id, email, expiry_time, server_id): try: - session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + session = await login_with_credentials( + server_id, ADMIN_USERNAME, ADMIN_PASSWORD + ) response = await add_client( - session, server_id, client_id, email, tg_id, - limit_ip=1, total_gb=0, expiry_time=expiry_time, - enable=True, flow="xtls-rprx-vision" + session, + server_id, + client_id, + email, + tg_id, + limit_ip=1, + total_gb=0, + expiry_time=expiry_time, + enable=True, + flow="xtls-rprx-vision", ) if not response.get("success"): - logger.error(f"Ошибка при обновлении ключа на сервере {server_id} для {client_id}") + logger.error( + f"Ошибка при обновлении ключа на сервере {server_id} для {client_id}" + ) else: logger.info(f"Ключ успешно обновлен на сервере {server_id} для {client_id}") except Exception as e: - logger.error(f"Ошибка при обновлении ключа на сервере {server_id} для {client_id}: {e}") + logger.error( + f"Ошибка при обновлении ключа на сервере {server_id} для {client_id}: {e}" + ) -@router.callback_query(lambda c: c.data.startswith('delete_key|')) + +@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('|')[1] + client_id = callback_query.data.split("|")[1] try: try: - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) + await bot.delete_message( + chat_id=tg_id, message_id=callback_query.message.message_id + ) except Exception: - pass + pass - confirmation_keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton(text='✅ Да, удалить', callback_data=f'confirm_delete|{client_id}')], - [types.InlineKeyboardButton(text='❌ Нет, отменить', callback_data='view_keys')] - ]) + 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.send_message( chat_id=tg_id, text="Вы уверены, что хотите удалить ключ?", reply_markup=confirmation_keyboard, - parse_mode="HTML" + parse_mode="HTML", ) except Exception as e: await bot.send_message( chat_id=tg_id, text=f"Ошибка при удалении ключа: {e}", - parse_mode="HTML" + parse_mode="HTML", ) await callback_query.answer() -@router.callback_query(lambda c: c.data.startswith('renew_key|')) + +@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('|')[1] + client_id = callback_query.data.split("|")[1] try: try: - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) + await bot.delete_message( + chat_id=tg_id, message_id=callback_query.message.message_id + ) except Exception: - pass + pass conn = await asyncpg.connect(DATABASE_URL) try: - record = await conn.fetchrow('SELECT email, expiry_time FROM keys WHERE client_id = $1', client_id) + 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 - keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton(text=f'📅 1 месяц ({RENEWAL_PLANS["1"]["price"]} руб.)', callback_data=f'renew_plan|1|{client_id}')], - [types.InlineKeyboardButton(text=f'📅 3 месяца ({RENEWAL_PLANS["3"]["price"]} руб.)', callback_data=f'renew_plan|3|{client_id}')], - [types.InlineKeyboardButton(text=f'📅 6 месяцев ({RENEWAL_PLANS["6"]["price"]} руб.)', callback_data=f'renew_plan|6|{client_id}')], - [types.InlineKeyboardButton(text=f'📅 12 месяцев ({RENEWAL_PLANS["12"]["price"]} руб.)', callback_data=f'renew_plan|12|{client_id}')], - [types.InlineKeyboardButton(text='🔙 Назад', callback_data='view_profile')] - ]) + email = record["email"] + expiry_time = record["expiry_time"] + current_time = datetime.utcnow().timestamp() * 1000 + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text=f'📅 1 месяц ({RENEWAL_PLANS["1"]["price"]} руб.)', + callback_data=f"renew_plan|1|{client_id}", + ) + ], + [ + types.InlineKeyboardButton( + text=f'📅 3 месяца ({RENEWAL_PLANS["3"]["price"]} руб.)', + callback_data=f"renew_plan|3|{client_id}", + ) + ], + [ + types.InlineKeyboardButton( + text=f'📅 6 месяцев ({RENEWAL_PLANS["6"]["price"]} руб.)', + callback_data=f"renew_plan|6|{client_id}", + ) + ], + [ + types.InlineKeyboardButton( + text=f'📅 12 месяцев ({RENEWAL_PLANS["12"]["price"]} руб.)', + callback_data=f"renew_plan|12|{client_id}", + ) + ], + [ + types.InlineKeyboardButton( + text="🔙 Назад", callback_data="view_profile" + ) + ], + ] + ) balance = await get_balance(tg_id) - response_message = PLAN_SELECTION_MSG.format(balance=balance, expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime('%Y-%m-%d %H:%M:%S')) + response_message = PLAN_SELECTION_MSG.format( + balance=balance, + expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime( + "%Y-%m-%d %H:%M:%S" + ), + ) - await bot.send_message(chat_id=tg_id, text=response_message, reply_markup=keyboard, parse_mode="HTML") + await bot.send_message( + chat_id=tg_id, + text=response_message, + reply_markup=keyboard, + parse_mode="HTML", + ) else: response_message = "Ключ не найден." - await bot.send_message(chat_id=tg_id, text=response_message, parse_mode="HTML") + await bot.send_message( + chat_id=tg_id, text=response_message, parse_mode="HTML" + ) finally: await conn.close() except Exception as e: - await bot.send_message(chat_id=tg_id, text=f"Ошибка при выборе плана: {e}", parse_mode="HTML") + await bot.send_message( + chat_id=tg_id, + text=f"Ошибка при выборе плана: {e}", + parse_mode="HTML", + ) await callback_query.answer() -@router.callback_query(lambda c: c.data.startswith('confirm_delete|')) + +@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('|')[1] + client_id = callback_query.data.split("|")[1] try: conn = await asyncpg.connect(DATABASE_URL) try: - record = await conn.fetchrow('SELECT email FROM keys WHERE client_id = $1', client_id) + record = await conn.fetchrow( + "SELECT email FROM keys WHERE client_id = $1", client_id + ) if record: - email = record['email'] + email = record["email"] response_message = "Ключ успешно удален." - back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys') + back_button = types.InlineKeyboardButton( + text="Назад", callback_data="view_keys" + ) keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) await delete_key(client_id) - await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard) + await bot.edit_message_text( + response_message, + chat_id=tg_id, + message_id=callback_query.message.message_id, + reply_markup=keyboard, + ) async def delete_key_from_servers(): try: tasks = [] for server_id in SERVERS: tasks.append(delete_key_from_server(server_id, client_id)) - + await asyncio.gather(*tasks) except Exception as e: @@ -387,16 +563,27 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery): else: response_message = "Ключ не найден или уже удален." - back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys') + 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) + 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 bot.edit_message_text( + f"Ошибка при удалении ключа: {e}", + chat_id=tg_id, + message_id=callback_query.message.message_id, + ) await callback_query.answer() @@ -404,74 +591,110 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery): async def delete_key_from_server(server_id, client_id): """Удаление ключа с сервера""" try: - session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + session = await login_with_credentials( + server_id, ADMIN_USERNAME, ADMIN_PASSWORD + ) success = await delete_client(session, server_id, client_id) if not success: logger.error(f"Ошибка удаления ключа {client_id} на сервере {server_id}") except Exception as e: - logger.error(f"Ошибка при удалении ключа {client_id} с сервера {server_id}: {e}") + logger.error( + f"Ошибка при удалении ключа {client_id} с сервера {server_id}: {e}" + ) async def delete_key_from_db(client_id): """Удаление ключа из базы данных""" try: conn = await asyncpg.connect(DATABASE_URL) - await conn.execute('DELETE FROM keys WHERE client_id = $1', client_id) + await conn.execute("DELETE FROM keys WHERE client_id = $1", client_id) except Exception as e: logger.error(f"Ошибка при удалении ключа {client_id} из базы данных: {e}") finally: await conn.close() -@router.callback_query(lambda c: c.data.startswith('renew_plan|')) +@router.callback_query(lambda c: c.data.startswith("renew_plan|")) 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('|')[2] - days_to_extend = 30 * int(plan) + plan, client_id = ( + callback_query.data.split("|")[1], + callback_query.data.split("|")[2], + ) + days_to_extend = 30 * int(plan) try: try: - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) + await bot.delete_message( + chat_id=tg_id, message_id=callback_query.message.message_id + ) except Exception: - pass + pass conn = await asyncpg.connect(DATABASE_URL) try: - record = await conn.fetchrow('SELECT email, expiry_time FROM keys WHERE client_id = $1', client_id) + 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'] + email = record["email"] + expiry_time = record["expiry_time"] current_time = datetime.utcnow().timestamp() * 1000 if expiry_time <= current_time: - new_expiry_time = int(current_time + timedelta(days=days_to_extend).total_seconds() * 1000) + new_expiry_time = int( + current_time + + timedelta(days=days_to_extend).total_seconds() * 1000 + ) else: - new_expiry_time = int(expiry_time + timedelta(days=days_to_extend).total_seconds() * 1000) + new_expiry_time = int( + expiry_time + + timedelta(days=days_to_extend).total_seconds() * 1000 + ) - cost = RENEWAL_PLANS[plan]['price'] + cost = RENEWAL_PLANS[plan]["price"] 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_profile') - keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[replenish_button], [back_button]]) + replenish_button = types.InlineKeyboardButton( + text="Пополнить баланс", callback_data="replenish_balance" + ) + back_button = types.InlineKeyboardButton( + text="Назад", callback_data="view_profile" + ) + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[[replenish_button], [back_button]] + ) - await bot.send_message(tg_id, INSUFFICIENT_FUNDS_MSG, reply_markup=keyboard, parse_mode="HTML") + await bot.send_message( + tg_id, + INSUFFICIENT_FUNDS_MSG, + reply_markup=keyboard, + parse_mode="HTML", + ) return - response_message = SUCCESS_RENEWAL_MSG.format(months=RENEWAL_PLANS[plan]['months']) - back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_profile') + response_message = SUCCESS_RENEWAL_MSG.format( + months=RENEWAL_PLANS[plan]["months"] + ) + back_button = types.InlineKeyboardButton( + text="Назад", callback_data="view_profile" + ) keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]]) - await bot.send_message(tg_id, response_message, reply_markup=keyboard, parse_mode="HTML") + await bot.send_message( + tg_id, response_message, reply_markup=keyboard, parse_mode="HTML" + ) async def renew_key_on_servers(): tasks = [] for server_id in SERVERS: task = asyncio.create_task( - renew_server_key(server_id, tg_id, client_id, email, new_expiry_time) + renew_server_key( + server_id, tg_id, client_id, email, new_expiry_time + ) ) tasks.append(task) @@ -489,27 +712,37 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery): await conn.close() except Exception as e: - await bot.send_message(tg_id, f"Ошибка при продлении ключа: {e}", parse_mode="HTML") + await bot.send_message( + tg_id, f"Ошибка при продлении ключа: {e}", parse_mode="HTML" + ) await callback_query.answer() + async def renew_server_key(server_id, tg_id, client_id, email, new_expiry_time): try: - session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) - await extend_client_key(session, server_id, tg_id, client_id, email, new_expiry_time) + session = await login_with_credentials( + server_id, ADMIN_USERNAME, ADMIN_PASSWORD + ) + await extend_client_key( + session, server_id, tg_id, client_id, email, new_expiry_time + ) except Exception as e: - logger.error(f"Не удалось продлить ключ {client_id} на сервере {server_id}: {e}") + logger.error( + f"Не удалось продлить ключ {client_id} на сервере {server_id}: {e}" + ) async def handle_error(tg_id, callback_query, message): try: try: - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) + await bot.delete_message( + chat_id=tg_id, message_id=callback_query.message.message_id + ) except Exception: - pass + pass await bot.send_message(tg_id, message, parse_mode="HTML") except Exception as e: logger.error(f"Ошибка при обработке ошибки: {e}") - diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index 78a0c767..d1f44154 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -6,7 +6,9 @@ from aiohttp import web from config import SERVERS -logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') +logging.basicConfig( + level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s" +) logger = logging.getLogger(__name__) @@ -18,54 +20,60 @@ async def fetch_url_content(url): if response.status == 200: content = await response.text() logger.debug(f"Успешно получен контент с {url}") - return base64.b64decode(content).decode('utf-8').split("\n") + return base64.b64decode(content).decode("utf-8").split("\n") else: - logger.error(f"Не удалось получить {url}, статус: {response.status}") - return [] + logger.error( + f"Не удалось получить {url}, статус: {response.status}" + ) + return [] except Exception as e: logger.error(f"Ошибка при получении {url}: {e}") - return [] + return [] async def combine_unique_lines(urls, query_string): all_lines = [] logger.debug(f"Начинаем объединение подписок для запроса: {query_string}") - + urls_with_query = [f"{url}?{query_string}" for url in urls] logger.debug(f"Составлены URL-адреса: {urls_with_query}") - + for url in urls_with_query: lines = await fetch_url_content(url) all_lines.extend(lines) - - all_lines = list(set(filter(None, all_lines))) - logger.debug(f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов") - + all_lines = list(set(filter(None, all_lines))) + logger.debug( + f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов" + ) + return all_lines + async def handle_subscription(request): - email = request.match_info['email'] + email = request.match_info["email"] logger.info(f"Получен запрос на подписку для email: {email}") urls = [] for server in SERVERS.values(): server_subscription_url = f"{server['SUBSCRIPTION']}/{email}" urls.append(server_subscription_url) - + query_string = request.query_string logger.debug(f"Извлечен query string: {query_string}") - + combined_subscriptions = await combine_unique_lines(urls, query_string) - - base64_encoded = base64.b64encode("\n".join(combined_subscriptions).encode('utf-8')).decode('utf-8') - + + base64_encoded = base64.b64encode( + "\n".join(combined_subscriptions).encode("utf-8") + ).decode("utf-8") + headers = { - 'Content-Type': 'text/plain; charset=utf-8', - 'Content-Disposition': 'inline', - 'profile-update-interval': '7', - 'profile-title': email, + "Content-Type": "text/plain; charset=utf-8", + "Content-Disposition": "inline", + "profile-update-interval": "7", + "profile-title": email, } logger.info(f"Возвращаем объединенные подписки для email: {email}") - return web.Response(text=base64_encoded, headers=headers) \ No newline at end of file + return web.Response(text=base64_encoded, headers=headers) diff --git a/handlers/keys/trial_key.py b/handlers/keys/trial_key.py index c5dad1c7..4359431b 100644 --- a/handlers/keys/trial_key.py +++ b/handlers/keys/trial_key.py @@ -1,4 +1,5 @@ import asyncio +import logging import uuid from datetime import datetime, timedelta @@ -6,12 +7,13 @@ import asyncpg from auth import login_with_credentials from client import add_client -from config import (ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, PUBLIC_LINK, - SERVERS) +from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, PUBLIC_LINK, SERVERS from database import store_key from handlers.texts import INSTRUCTIONS from handlers.utils import generate_random_email +logging.basicConfig(level=logging.DEBUG) + async def create_trial_key(tg_id: int): conn = await asyncpg.connect(DATABASE_URL) @@ -21,20 +23,22 @@ async def create_trial_key(tg_id: int): public_link = f"{PUBLIC_LINK}{email}" instructions = INSTRUCTIONS - - result = { - 'key': public_link, - 'instructions': instructions - } - - asyncio.create_task(generate_and_store_keys(tg_id, client_id, email, public_link)) - + + result = {"key": public_link, "instructions": instructions} + + asyncio.create_task( + generate_and_store_keys(tg_id, client_id, email, public_link) + ) + return result finally: await conn.close() -async def generate_and_store_keys(tg_id: int, client_id: str, email: str, public_link: str): + +async def generate_and_store_keys( + tg_id: int, client_id: str, email: str, public_link: str +): conn = await asyncpg.connect(DATABASE_URL) try: current_time = datetime.utcnow() @@ -43,34 +47,54 @@ async def generate_and_store_keys(tg_id: int, client_id: str, email: str, public tasks = [] for server_id in SERVERS: - task = create_key_on_server(server_id, client_id, email, tg_id, expiry_timestamp) + task = create_key_on_server( + server_id, client_id, email, tg_id, expiry_timestamp + ) tasks.append(task) - + results = await asyncio.gather(*tasks) if all(result.get("success") for result in results): - await store_key(tg_id, client_id, email, expiry_timestamp, public_link, server_id="all_servers") + await store_key( + tg_id, + client_id, + email, + expiry_timestamp, + public_link, + server_id="all_servers", + ) - - await conn.execute(''' + await conn.execute( + """ INSERT INTO connections (tg_id, trial) VALUES ($1, 1) ON CONFLICT (tg_id) DO UPDATE SET trial = 1 - ''', tg_id) + """, + tg_id, + ) else: - print('Не удалось создать ключ на одном или нескольких серверах.') + logging.error("Не удалось создать ключ на одном или нескольких серверах.") finally: await conn.close() -async def create_key_on_server(server_id: str, client_id: str, email: str, tg_id: int, expiry_timestamp: int): +async def create_key_on_server( + server_id: str, client_id: str, email: str, tg_id: int, expiry_timestamp: int +): """Асинхронно создает ключ на указанном сервере и возвращает результат.""" session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) response = await add_client( - session, server_id, client_id, email, tg_id, - limit_ip=1, total_gb=0, expiry_time=expiry_timestamp, - enable=True, flow="xtls-rprx-vision" + session, + server_id, + client_id, + email, + tg_id, + limit_ip=1, + total_gb=0, + expiry_time=expiry_timestamp, + enable=True, + flow="xtls-rprx-vision", ) return response diff --git a/handlers/notifications.py b/handlers/notifications.py index 4fd4c05b..92e5d6a0 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -10,33 +10,41 @@ from auth import login_with_credentials from client import delete_client, extend_client_key from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, SERVERS from database import delete_key, get_balance, update_balance, update_key_expiry -from handlers.texts import (KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWAL_FAILED, - KEY_RENEWED) +from handlers.texts import ( + KEY_EXPIRY_10H, + KEY_EXPIRY_24H, + KEY_RENEWAL_FAILED, + KEY_RENEWED, +) logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) router = Router() + class NotificationStates(StatesGroup): waiting_for_notification_text = State() + async def notify_expiring_keys(bot: Bot): conn = None try: conn = await asyncpg.connect(DATABASE_URL) logger.info("Подключение к базе данных успешно.") - + current_time = datetime.utcnow().timestamp() * 1000 - threshold_time_10h = (datetime.utcnow() + timedelta(hours=10)).timestamp() * 1000 - threshold_time_24h = (datetime.utcnow() + timedelta(days=1)).timestamp() * 1000 + threshold_time_10h = ( + datetime.utcnow() + timedelta(hours=10) + ).timestamp() * 1000 + threshold_time_24h = (datetime.utcnow() + timedelta(days=1)).timestamp() * 1000 logger.info("Начало обработки уведомлений.") await notify_10h_keys(bot, conn, current_time, threshold_time_10h) - await asyncio.sleep(1) + await asyncio.sleep(1) await notify_24h_keys(bot, conn, current_time, threshold_time_24h) - await asyncio.sleep(1) + await asyncio.sleep(1) await handle_expired_keys(bot, conn, current_time) except Exception as e: @@ -46,25 +54,33 @@ async def notify_expiring_keys(bot: Bot): await conn.close() logger.info("Соединение с базой данных закрыто.") + async def is_bot_blocked(bot: Bot, chat_id: int) -> bool: try: member = await bot.get_chat_member(chat_id, bot.id) - return member.status == 'left' + return member.status == "left" except Exception as e: logger.error(f"Ошибка при проверке статуса бота у пользователя {chat_id}: {e}") - return False + return False -async def notify_10h_keys(bot: Bot, conn: asyncpg.Connection, current_time: float, threshold_time_10h: float): - records = await conn.fetch(''' + +async def notify_10h_keys( + bot: Bot, conn: asyncpg.Connection, current_time: float, threshold_time_10h: float +): + records = await conn.fetch( + """ SELECT tg_id, email, expiry_time, client_id, server_id FROM keys WHERE expiry_time <= $1 AND expiry_time > $2 AND notified = FALSE - ''', threshold_time_10h, current_time) + """, + threshold_time_10h, + current_time, + ) logger.info(f"Найдено {len(records)} ключей для уведомления за 10 часов.") for record in records: - tg_id = record['tg_id'] - email = record['email'] - expiry_time = record['expiry_time'] + tg_id = record["tg_id"] + email = record["email"] + expiry_time = record["expiry_time"] expiry_date = datetime.utcfromtimestamp(expiry_time / 1000) current_date = datetime.utcnow() @@ -79,42 +95,70 @@ async def notify_10h_keys(bot: Bot, conn: asyncpg.Connection, current_time: floa days_left_message = f"{hours_left}" message = KEY_EXPIRY_10H.format( - email=email, - expiry_date=expiry_date.strftime('%Y-%m-%d %H:%M:%S'), - days_left_message=days_left_message + email=email, + expiry_date=expiry_date.strftime("%Y-%m-%d %H:%M:%S"), + days_left_message=days_left_message, ) if not await is_bot_blocked(bot, tg_id): try: - keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton(text='🔄 Продлить VPN', callback_data=f'renew_key|{record["client_id"]}')], - [types.InlineKeyboardButton(text='💳 Пополнить баланс', callback_data='replenish_balance')], - [types.InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')] - ]) + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text="🔄 Продлить VPN", + callback_data=f'renew_key|{record["client_id"]}', + ) + ], + [ + types.InlineKeyboardButton( + text="💳 Пополнить баланс", + callback_data="replenish_balance", + ) + ], + [ + types.InlineKeyboardButton( + text="👤 Мой профиль", callback_data="view_profile" + ) + ], + ] + ) await bot.send_message(tg_id, message, reply_markup=keyboard) logger.info(f"Уведомление отправлено пользователю {tg_id}.") except Exception as e: - logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}") - continue + logger.error( + f"Ошибка при отправке уведомления пользователю {tg_id}: {e}" + ) + continue - await conn.execute('UPDATE keys SET notified = TRUE WHERE client_id = $1', record['client_id']) + await conn.execute( + "UPDATE keys SET notified = TRUE WHERE client_id = $1", + record["client_id"], + ) logger.info(f"Обновлено поле notified для клиента {record['client_id']}.") - - await asyncio.sleep(1) -async def notify_24h_keys(bot: Bot, conn: asyncpg.Connection, current_time: float, threshold_time_24h: float): + await asyncio.sleep(1) + + +async def notify_24h_keys( + bot: Bot, conn: asyncpg.Connection, current_time: float, threshold_time_24h: float +): logger.info("Проверка истекших ключей...") - records_24h = await conn.fetch(''' + records_24h = await conn.fetch( + """ SELECT tg_id, email, expiry_time, client_id, server_id FROM keys WHERE expiry_time <= $1 AND expiry_time > $2 AND notified_24h = FALSE - ''', threshold_time_24h, current_time) + """, + threshold_time_24h, + current_time, + ) logger.info(f"Найдено {len(records_24h)} ключей для уведомления за 24 часа.") for record in records_24h: - tg_id = record['tg_id'] - email = record['email'] - expiry_time = record['expiry_time'] + tg_id = record["tg_id"] + email = record["email"] + expiry_time = record["expiry_time"] expiry_date = datetime.utcfromtimestamp(expiry_time / 1000) current_date = datetime.utcnow() @@ -131,26 +175,49 @@ async def notify_24h_keys(bot: Bot, conn: asyncpg.Connection, current_time: floa message_24h = KEY_EXPIRY_24H.format( email=email, days_left_message=days_left_message, - expiry_date=expiry_date.strftime('%Y-%m-%d %H:%M:%S') + expiry_date=expiry_date.strftime("%Y-%m-%d %H:%M:%S"), ) if not await is_bot_blocked(bot, tg_id): try: - keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton(text='🔄 Продлить VPN', callback_data=f'renew_key|{record["client_id"]}')], - [types.InlineKeyboardButton(text='💳 Пополнить баланс', callback_data='replenish_balance')], - [types.InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')] - ]) + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text="🔄 Продлить VPN", + callback_data=f'renew_key|{record["client_id"]}', + ) + ], + [ + types.InlineKeyboardButton( + text="💳 Пополнить баланс", + callback_data="replenish_balance", + ) + ], + [ + types.InlineKeyboardButton( + text="👤 Мой профиль", callback_data="view_profile" + ) + ], + ] + ) await bot.send_message(tg_id, message_24h, reply_markup=keyboard) logger.info(f"Уведомление за 24 часа отправлено пользователю {tg_id}.") except Exception as e: - logger.error(f"Ошибка при отправке уведомления за 24 часа пользователю {tg_id}: {e}") - continue + logger.error( + f"Ошибка при отправке уведомления за 24 часа пользователю {tg_id}: {e}" + ) + continue - await conn.execute('UPDATE keys SET notified_24h = TRUE WHERE client_id = $1', record['client_id']) - logger.info(f"Обновлено поле notified_24h для клиента {record['client_id']}.") - - await asyncio.sleep(1) + await conn.execute( + "UPDATE keys SET notified_24h = TRUE WHERE client_id = $1", + record["client_id"], + ) + logger.info( + f"Обновлено поле notified_24h для клиента {record['client_id']}." + ) + + await asyncio.sleep(1) async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: float): @@ -159,26 +226,33 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: current_time = datetime.utcnow().timestamp() * 1000 adjusted_current_time = current_time + (3 * 60 * 60 * 1000) - logger.info(f"Текущее время: {current_time}, Скорректированное текущее время: {adjusted_current_time}") + logger.info( + f"Текущее время: {current_time}, Скорректированное текущее время: {adjusted_current_time}" + ) - expiring_keys = await conn.fetch(''' + expiring_keys = await conn.fetch( + """ SELECT tg_id, client_id, expiry_time, email FROM keys WHERE expiry_time <= $1 - ''', adjusted_current_time) + """, + adjusted_current_time, + ) logger.info(f"Найдено {len(expiring_keys)} истекающих ключей.") for record in expiring_keys: - tg_id = record['tg_id'] - client_id = record['client_id'] - email = record['email'] + tg_id = record["tg_id"] + client_id = record["client_id"] + email = record["email"] balance = await get_balance(tg_id) - expiry_time = record['expiry_time'] + expiry_time = record["expiry_time"] expiry_date = datetime.utcfromtimestamp(expiry_time / 1000) current_date = datetime.utcnow() time_left = expiry_date - current_date - logger.info(f"Время истечения ключа: {expiry_time} (дата: {expiry_date}), Текущее время: {current_date}, Оставшееся время: {time_left}.") + logger.info( + f"Время истечения ключа: {expiry_time} (дата: {expiry_date}), Текущее время: {current_date}, Оставшееся время: {time_left}." + ) if time_left.total_seconds() <= 0: days_left_message = "Ключ истек" @@ -189,70 +263,114 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: days_left_message = f"Осталось часов: {hours_left}" message_expired = f"Ваш ключ {email} истек и был удален!\n\n Перейдите в профиль для создания нового ключа" - button_profile = types.InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile') + button_profile = types.InlineKeyboardButton( + text="👤 Мой профиль", callback_data="view_profile" + ) keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[button_profile]]) if balance >= 100: await update_balance(tg_id, -100) - new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000) + new_expiry_time = int( + (datetime.utcnow() + timedelta(days=30)).timestamp() * 1000 + ) await update_key_expiry(client_id, new_expiry_time) - logger.info(f"Ключ для клиента {tg_id} продлен до {datetime.utcfromtimestamp(new_expiry_time / 1000).strftime('%Y-%m-%d %H:%M:%S')}.") + logger.info( + f"Ключ для клиента {tg_id} продлен до {datetime.utcfromtimestamp(new_expiry_time / 1000).strftime('%Y-%m-%d %H:%M:%S')}." + ) all_success = True for server_id in SERVERS: - session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) - success = await extend_client_key(session, server_id, tg_id, client_id, email, new_expiry_time) + session = await login_with_credentials( + server_id, ADMIN_USERNAME, ADMIN_PASSWORD + ) + success = await extend_client_key( + session, server_id, tg_id, client_id, email, new_expiry_time + ) if not success: all_success = False - logger.error(f"Не удалось продлить ключ для пользователя {tg_id} на сервере {server_id}.") + logger.error( + f"Не удалось продлить ключ для пользователя {tg_id} на сервере {server_id}." + ) if all_success: try: await bot.send_message(tg_id, KEY_RENEWED, reply_markup=keyboard) - logger.info(f"Ключ для пользователя {tg_id} успешно продлен на месяц на всех серверах.") + logger.info( + f"Ключ для пользователя {tg_id} успешно продлен на месяц на всех серверах." + ) except Exception as e: - if 'blocked' in str(e).lower(): - logger.warning(f"Пользователь {tg_id} заблокирован. Ключ будет удален.") + if "blocked" in str(e).lower(): + logger.warning( + f"Пользователь {tg_id} заблокирован. Ключ будет удален." + ) await delete_key(client_id) for server_id in SERVERS: - session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + session = await login_with_credentials( + server_id, ADMIN_USERNAME, ADMIN_PASSWORD + ) success = await delete_client(session, server_id, client_id) if success: - logger.info(f"Ключ для клиента {tg_id} успешно удален с сервера {server_id}.") + logger.info( + f"Ключ для клиента {tg_id} успешно удален с сервера {server_id}." + ) else: - logger.error(f"Не удалось удалить ключ для клиента {tg_id} на сервере {server_id}.") + logger.error( + f"Не удалось удалить ключ для клиента {tg_id} на сервере {server_id}." + ) else: - logger.error(f"Ошибка при отправке уведомления о продлении ключа пользователю {tg_id}: {e}") + logger.error( + f"Ошибка при отправке уведомления о продлении ключа пользователю {tg_id}: {e}" + ) else: try: - await bot.send_message(tg_id, KEY_RENEWAL_FAILED, reply_markup=keyboard) - logger.error(f"Не удалось продлить ключ для пользователя {tg_id} на одном или нескольких серверах.") + await bot.send_message( + tg_id, KEY_RENEWAL_FAILED, reply_markup=keyboard + ) + logger.error( + f"Не удалось продлить ключ для пользователя {tg_id} на одном или нескольких серверах." + ) except Exception as e: - logger.error(f"Ошибка при отправке уведомления о неудачном продлении ключа пользователю {tg_id}: {e}") + logger.error( + f"Ошибка при отправке уведомления о неудачном продлении ключа пользователю {tg_id}: {e}" + ) else: try: await bot.send_message(tg_id, message_expired, reply_markup=keyboard) await delete_key(client_id) for server_id in SERVERS: - session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + session = await login_with_credentials( + server_id, ADMIN_USERNAME, ADMIN_PASSWORD + ) success = await delete_client(session, server_id, client_id) if success: - logger.info(f"Ключ для клиента {tg_id} успешно удален с сервера {server_id}.") + logger.info( + f"Ключ для клиента {tg_id} успешно удален с сервера {server_id}." + ) else: - logger.error(f"Не удалось удалить ключ для клиента {tg_id} на сервере {server_id}.") + logger.error( + f"Не удалось удалить ключ для клиента {tg_id} на сервере {server_id}." + ) except Exception as e: - if 'blocked' in str(e).lower(): - logger.warning(f"Пользователь {tg_id} заблокирован. Ключ будет удален.") + if "blocked" in str(e).lower(): + logger.warning( + f"Пользователь {tg_id} заблокирован. Ключ будет удален." + ) await delete_key(client_id) for server_id in SERVERS: - session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD) + session = await login_with_credentials( + server_id, ADMIN_USERNAME, ADMIN_PASSWORD + ) success = await delete_client(session, server_id, client_id) if success: - logger.info(f"Ключ для клиента {tg_id} успешно удален с сервера {server_id}.") + logger.info( + f"Ключ для клиента {tg_id} успешно удален с сервера {server_id}." + ) else: - logger.error(f"Не удалось удалить ключ для клиента {tg_id} на сервере {server_id}.") + logger.error( + f"Не удалось удалить ключ для клиента {tg_id} на сервере {server_id}." + ) else: logger.error(f"Ошибка при удалении ключа для клиента {tg_id}: {e}") - await asyncio.sleep(1) \ No newline at end of file + await asyncio.sleep(1) diff --git a/handlers/payment/freekassa.py b/handlers/payment/freekassa_pay.py similarity index 58% rename from handlers/payment/freekassa.py rename to handlers/payment/freekassa_pay.py index 29295dd8..fa8fe92a 100644 --- a/handlers/payment/freekassa.py +++ b/handlers/payment/freekassa_pay.py @@ -17,15 +17,18 @@ from database import update_balance router = Router() logging.basicConfig(level=logging.DEBUG) + class ReplenishBalanceState(StatesGroup): choosing_amount = State() waiting_for_payment_confirmation = State() entering_custom_amount = State() + def generate_signature(params, api_key): sign_string = ":".join([str(params[k]) for k in sorted(params)]) + api_key return hashlib.md5(sign_string.encode()).hexdigest() + async def create_payment(user_id, amount, email, ip): payment_id = str(uuid.uuid4()) nonce = int(time.time() * 1000) @@ -36,19 +39,21 @@ async def create_payment(user_id, amount, email, ip): "paymentId": payment_id, "email": email, "ip": ip, - "i": 6, - "nonce": nonce + "i": 6, + "nonce": nonce, } params["signature"] = generate_signature(params, FREEKASSA_API_KEY) try: - response = requests.post("https://api.freekassa.com/v1/orders/create", json=params) + response = requests.post( + "https://api.freekassa.com/v1/orders/create", json=params + ) response_data = response.json() - + logging.debug(f"Ответ от FreeKassa при создании платежа: {response_data}") if response_data.get("type") == "success": - return response_data["location"] + return response_data["location"] else: logging.error(f"Ошибка создания платежа: {response_data}") return None @@ -57,15 +62,17 @@ async def create_payment(user_id, amount, email, ip): logging.error(f"Ошибка запроса к FreeKassa: {e}") return None + async def send_payment_success_notification(user_id, amount): try: await bot.send_message( chat_id=user_id, - text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!" + text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!", ) except Exception as e: logging.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}") + async def freekassa_webhook(request): data = await request.json() logging.debug(f"Получен вебхук от FreeKassa: {data}") @@ -81,38 +88,50 @@ async def freekassa_webhook(request): return web.Response(status=200) -@router.callback_query(lambda c: c.data == 'pay_freekassa') -async def process_callback_pay_freekassa(callback_query: types.CallbackQuery, state: FSMContext): + +@router.callback_query(lambda c: c.data == "pay_freekassa") +async def process_callback_pay_freekassa( + callback_query: types.CallbackQuery, state: FSMContext +): tg_id = callback_query.from_user.id - - amount_keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [ - InlineKeyboardButton(text="100 рублей", callback_data="amount|100"), - InlineKeyboardButton(text="500 рублей", callback_data="amount|500") - ], - [ - InlineKeyboardButton(text="1000 рублей", callback_data="amount|1000"), - InlineKeyboardButton(text="5000 рублей", callback_data="amount|5000") - ], - [ - InlineKeyboardButton(text="Введите другую сумму", callback_data="enter_custom_amount") + + amount_keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text="100 рублей", callback_data="amount|100"), + InlineKeyboardButton(text="500 рублей", callback_data="amount|500"), + ], + [ + InlineKeyboardButton(text="1000 рублей", callback_data="amount|1000"), + InlineKeyboardButton(text="5000 рублей", callback_data="amount|5000"), + ], + [ + InlineKeyboardButton( + text="Введите другую сумму", callback_data="enter_custom_amount" + ) + ], ] - ]) - - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) + ) + + await bot.delete_message( + chat_id=tg_id, message_id=callback_query.message.message_id + ) await bot.send_message( chat_id=tg_id, text="Выберите сумму пополнения через FreeKassa:", - reply_markup=amount_keyboard + reply_markup=amount_keyboard, ) - + await state.set_state(ReplenishBalanceState.choosing_amount) await callback_query.answer() -@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) + +@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) amount_str = data[1] try: amount = int(amount_str) @@ -120,39 +139,55 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F await bot.send_message(callback_query.from_user.id, "Некорректная сумма.") return - user_email = f"{callback_query.from_user.id}@solo.net" - user_ip = callback_query.message.chat.id - payment_url = await create_payment(callback_query.from_user.id, amount, user_email, user_ip) + user_email = f"{callback_query.from_user.id}@solo.net" + user_ip = callback_query.message.chat.id + payment_url = await create_payment( + callback_query.from_user.id, amount, user_email, user_ip + ) if payment_url: - await bot.send_message(callback_query.from_user.id, f"Перейдите по ссылке для оплаты: {payment_url}") + await bot.send_message( + callback_query.from_user.id, + f"Перейдите по ссылке для оплаты: {payment_url}", + ) else: - await bot.send_message(callback_query.from_user.id, "Ошибка при создании платежа. Попробуйте позже.") + await bot.send_message( + callback_query.from_user.id, + "Ошибка при создании платежа. Попробуйте позже.", + ) await callback_query.answer() -@router.callback_query(lambda c: c.data == 'enter_custom_amount') -async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): - await callback_query.message.edit_text( - text="Введите сумму пополнения:" - ) + +@router.callback_query(lambda c: c.data == "enter_custom_amount") +async def process_enter_custom_amount( + callback_query: types.CallbackQuery, state: FSMContext +): + await callback_query.message.edit_text(text="Введите сумму пополнения:") await state.set_state(ReplenishBalanceState.entering_custom_amount) await callback_query.answer() + @router.message(ReplenishBalanceState.entering_custom_amount) async def process_custom_amount_input(message: types.Message, state: FSMContext): if message.text.isdigit(): amount = int(message.text) if amount <= 0: - await message.answer("Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:") + await message.answer( + "Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:" + ) return - + user_email = f"{message.from_user.id}@solo.net" user_ip = message.chat.id - payment_url = await create_payment(message.from_user.id, amount, user_email, user_ip) + payment_url = await create_payment( + message.from_user.id, amount, user_email, user_ip + ) if payment_url: - await bot.send_message(message.from_user.id, f"Перейдите по ссылке для оплаты: {payment_url}") + await bot.send_message( + message.from_user.id, f"Перейдите по ссылке для оплаты: {payment_url}" + ) else: await message.answer("Ошибка при создании платежа. Попробуйте позже.") diff --git a/handlers/payment/pay.py b/handlers/payment/pay.py deleted file mode 100644 index fe47bf09..00000000 --- a/handlers/payment/pay.py +++ /dev/null @@ -1,280 +0,0 @@ -import logging -import uuid - -from aiogram import Router, types -from aiogram.fsm.context import FSMContext -from aiogram.fsm.state import State, StatesGroup -from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup -from aiohttp import web -from yookassa import Configuration, Payment - -from bot import bot -from config import YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID -from database import (add_connection, check_connection_exists, get_key_count, - update_balance) -from handlers.profile import process_callback_view_profile -from handlers.texts import PAYMENT_OPTIONS - -router = Router() - -logging.basicConfig(level=logging.DEBUG) - -Configuration.account_id = YOOKASSA_SHOP_ID -Configuration.secret_key = YOOKASSA_SECRET_KEY - -logging.debug(f"Account ID: {YOOKASSA_SHOP_ID}") -logging.debug(f"Secret Key: {YOOKASSA_SECRET_KEY}") - -class ReplenishBalanceState(StatesGroup): - choosing_amount = State() - waiting_for_payment_confirmation = State() - entering_custom_amount = State() - -async def send_message_with_deletion(chat_id, text, reply_markup=None, state=None, message_key='last_message_id'): - if state: - try: - state_data = await state.get_data() - previous_message_id = state_data.get(message_key) - - if previous_message_id: - await bot.delete_message(chat_id=chat_id, message_id=previous_message_id) - - sent_message = await bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup) - await state.update_data({message_key: sent_message.message_id}) - - except Exception as e: - print(f"Ошибка при удалении/отправке сообщения: {e}") - return None - - return sent_message - -@router.callback_query(lambda c: c.data == 'replenish_balance') -async def process_callback_replenish_balance(callback_query: types.CallbackQuery, state: FSMContext): - tg_id = callback_query.from_user.id - - key_count = await get_key_count(tg_id) - - if key_count == 0: - exists = await check_connection_exists(tg_id) - if not exists: - await add_connection(tg_id, balance=0.0, trial=0) - - amount_keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [ - InlineKeyboardButton(text=PAYMENT_OPTIONS[0]['text'], callback_data=PAYMENT_OPTIONS[0]['callback_data']), - InlineKeyboardButton(text=PAYMENT_OPTIONS[1]['text'], callback_data=PAYMENT_OPTIONS[1]['callback_data']) - ], - [ - InlineKeyboardButton(text=PAYMENT_OPTIONS[2]['text'], callback_data=PAYMENT_OPTIONS[2]['callback_data']), - InlineKeyboardButton(text=PAYMENT_OPTIONS[3]['text'], callback_data=PAYMENT_OPTIONS[3]['callback_data']) - ], - [ - InlineKeyboardButton(text=PAYMENT_OPTIONS[4]['text'], callback_data=PAYMENT_OPTIONS[4]['callback_data']) - ], - [ - InlineKeyboardButton(text=PAYMENT_OPTIONS[5]['text'], callback_data=PAYMENT_OPTIONS[5]['callback_data']) - ], - ]) - - await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id) - - await bot.send_message( - chat_id=tg_id, - text="Выберите сумму пополнения:", - reply_markup=amount_keyboard - ) - - await state.set_state(ReplenishBalanceState.choosing_amount) - 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('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] - try: - amount = int(amount_str) - except ValueError: - await send_message_with_deletion(callback_query.from_user.id, "Некорректная сумма.", state=state, message_key='amount_error_message_id') - return - - await state.update_data(amount=amount) - await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation) - - state_data = await state.get_data() - customer_name = callback_query.from_user.full_name - customer_id = callback_query.from_user.id - - customer_email = f"{customer_id}@solo.net" - - payment = Payment.create({ - "amount": { - "value": str(amount), - "currency": "RUB" - }, - "confirmation": { - "type": "redirect", - "return_url": "https://pocomacho.ru/" - }, - "capture": True, - "description": "Пополнение баланса", - "receipt": { - "customer": { - "full_name": customer_name, - "email": customer_email, - "phone": "79000000000" - }, - "items": [ - { - "description": "Пополнение баланса", - "quantity": "1.00", - "amount": { - "value": str(amount), - "currency": "RUB" - }, - "vat_code": 6 - } - ] - }, - "metadata": { - "user_id": customer_id - } - }, uuid.uuid4()) - - if payment['status'] == 'pending': - payment_url = payment['confirmation']['confirmation_url'] - - confirm_keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='Пополнить', url=payment_url)], - [InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_profile')] - ]) - - await callback_query.message.edit_text( - text=f"Вы выбрали пополнение на {amount} рублей.", - reply_markup=confirm_keyboard - ) - else: - await send_message_with_deletion(callback_query.from_user.id, "Ошибка при создании платежа.", state=state) - - await callback_query.answer() - -async def send_payment_success_notification(user_id: int, amount: float): - try: - profile_keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='Перейти в профиль', callback_data='view_profile')] - ]) - - await bot.send_message( - chat_id=user_id, - text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!", - reply_markup=profile_keyboard - ) - except Exception as e: - logging.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}") - -async def payment_webhook(request): - event = await request.json() - - logging.debug(f"Webhook event received: {event}") - - if event['event'] == 'payment.succeeded': - user_id_str = event['object']['metadata']['user_id'] - amount_str = event['object']['amount']['value'] - - try: - user_id = int(user_id_str) - amount = float(amount_str) - - logging.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}") - await update_balance(user_id, amount) - - await send_payment_success_notification(user_id, amount) - - except ValueError as e: - logging.error(f"Ошибка конвертации user_id или amount: {e}") - return web.Response(status=400) - - return web.Response(status=200) - -@router.callback_query(lambda c: c.data == 'enter_custom_amount') -async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): - await callback_query.message.edit_text( - text="Введите сумму пополнения:" - ) - await state.set_state(ReplenishBalanceState.entering_custom_amount) - await callback_query.answer() - -@router.message(State(ReplenishBalanceState.entering_custom_amount)) -async def process_custom_amount_input(message: types.Message, state: FSMContext): - if message.text.isdigit(): - amount = int(message.text) - if amount <= 0: - await message.answer("Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:") - return - - await state.update_data(amount=amount) - await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation) - - try: - payment = Payment.create({ - "amount": { - "value": str(amount), - "currency": "RUB" - }, - "confirmation": { - "type": "redirect", - "return_url": "https://pocomacho.ru/" - }, - "capture": True, - "description": "Пополнение баланса", - "receipt": { - "customer": { - "full_name": message.from_user.full_name, - "email": f"{message.from_user.id}@solo.net", - "phone": "79000000000" - }, - "items": [ - { - "description": "Пополнение баланса", - "quantity": "1.00", - "amount": { - "value": str(amount), - "currency": "RUB" - }, - "vat_code": 6 - } - ] - }, - "metadata": { - "user_id": message.from_user.id - } - }, uuid.uuid4()) - - if payment['status'] == 'pending': - payment_url = payment['confirmation']['confirmation_url'] - - confirm_keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='Пополнить', url=payment_url)], - [InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_profile')] - ]) - - await message.answer( - text=f"Вы выбрали пополнение на {amount} рублей.", - reply_markup=confirm_keyboard - ) - else: - await message.answer("Ошибка при создании платежа.") - - except Exception as e: - logging.error(f"Ошибка при создании платежа: {e}") - await message.answer("Произошла ошибка при создании платежа.") - else: - await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:") diff --git a/handlers/payment/yookassa_pay.py b/handlers/payment/yookassa_pay.py new file mode 100644 index 00000000..756cc234 --- /dev/null +++ b/handlers/payment/yookassa_pay.py @@ -0,0 +1,340 @@ +import logging +import uuid + +from aiogram import Router, types +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from aiohttp import web +from yookassa import Configuration, Payment + +from bot import bot +from config import YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID +from database import ( + add_connection, + check_connection_exists, + get_key_count, + update_balance, +) +from handlers.profile import process_callback_view_profile +from handlers.texts import PAYMENT_OPTIONS + +router = Router() + +logging.basicConfig(level=logging.DEBUG) + +Configuration.account_id = YOOKASSA_SHOP_ID +Configuration.secret_key = YOOKASSA_SECRET_KEY + +logging.debug(f"Account ID: {YOOKASSA_SHOP_ID}") +logging.debug(f"Secret Key: {YOOKASSA_SECRET_KEY}") + + +class ReplenishBalanceState(StatesGroup): + choosing_amount = State() + waiting_for_payment_confirmation = State() + entering_custom_amount = State() + + +async def send_message_with_deletion( + chat_id, text, reply_markup=None, state=None, message_key="last_message_id" +): + if state: + try: + state_data = await state.get_data() + previous_message_id = state_data.get(message_key) + + if previous_message_id: + await bot.delete_message( + chat_id=chat_id, message_id=previous_message_id + ) + + sent_message = await bot.send_message( + chat_id=chat_id, text=text, reply_markup=reply_markup + ) + await state.update_data({message_key: sent_message.message_id}) + + except Exception as e: + logging.error(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: + exists = await check_connection_exists(tg_id) + if not exists: + await add_connection(tg_id, balance=0.0, trial=0) + + amount_keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text=PAYMENT_OPTIONS[0]["text"], + callback_data=PAYMENT_OPTIONS[0]["callback_data"], + ), + InlineKeyboardButton( + text=PAYMENT_OPTIONS[1]["text"], + callback_data=PAYMENT_OPTIONS[1]["callback_data"], + ), + ], + [ + InlineKeyboardButton( + text=PAYMENT_OPTIONS[2]["text"], + callback_data=PAYMENT_OPTIONS[2]["callback_data"], + ), + InlineKeyboardButton( + text=PAYMENT_OPTIONS[3]["text"], + callback_data=PAYMENT_OPTIONS[3]["callback_data"], + ), + ], + [ + InlineKeyboardButton( + text=PAYMENT_OPTIONS[4]["text"], + callback_data=PAYMENT_OPTIONS[4]["callback_data"], + ) + ], + [ + InlineKeyboardButton( + text=PAYMENT_OPTIONS[5]["text"], + callback_data=PAYMENT_OPTIONS[5]["callback_data"], + ) + ], + ] + ) + + await bot.delete_message( + chat_id=tg_id, message_id=callback_query.message.message_id + ) + + await bot.send_message( + chat_id=tg_id, text="Выберите сумму пополнения:", reply_markup=amount_keyboard + ) + + await state.set_state(ReplenishBalanceState.choosing_amount) + 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("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] + try: + amount = int(amount_str) + except ValueError: + await send_message_with_deletion( + callback_query.from_user.id, + "Некорректная сумма.", + state=state, + message_key="amount_error_message_id", + ) + return + + await state.update_data(amount=amount) + await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation) + + state_data = await state.get_data() + customer_name = callback_query.from_user.full_name + customer_id = callback_query.from_user.id + + customer_email = f"{customer_id}@solo.net" + + payment = Payment.create( + { + "amount": {"value": str(amount), "currency": "RUB"}, + "confirmation": {"type": "redirect", "return_url": "https://pocomacho.ru/"}, + "capture": True, + "description": "Пополнение баланса", + "receipt": { + "customer": { + "full_name": customer_name, + "email": customer_email, + "phone": "79000000000", + }, + "items": [ + { + "description": "Пополнение баланса", + "quantity": "1.00", + "amount": {"value": str(amount), "currency": "RUB"}, + "vat_code": 6, + } + ], + }, + "metadata": {"user_id": customer_id}, + }, + uuid.uuid4(), + ) + + if payment["status"] == "pending": + payment_url = payment["confirmation"]["confirmation_url"] + + confirm_keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Пополнить", url=payment_url)], + [InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile")], + ] + ) + + await callback_query.message.edit_text( + text=f"Вы выбрали пополнение на {amount} рублей.", + reply_markup=confirm_keyboard, + ) + else: + await send_message_with_deletion( + callback_query.from_user.id, "Ошибка при создании платежа.", state=state + ) + + await callback_query.answer() + + +async def send_payment_success_notification(user_id: int, amount: float): + try: + profile_keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="Перейти в профиль", callback_data="view_profile" + ) + ] + ] + ) + + await bot.send_message( + chat_id=user_id, + text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!", + reply_markup=profile_keyboard, + ) + except Exception as e: + logging.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}") + + +async def yookassa_webhook(request): + event = await request.json() + + logging.debug(f"Webhook event received: {event}") + + if event["event"] == "payment.succeeded": + user_id_str = event["object"]["metadata"]["user_id"] + amount_str = event["object"]["amount"]["value"] + + try: + user_id = int(user_id_str) + amount = float(amount_str) + + logging.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}") + await update_balance(user_id, amount) + + await send_payment_success_notification(user_id, amount) + + except ValueError as e: + logging.error(f"Ошибка конвертации user_id или amount: {e}") + return web.Response(status=400) + + return web.Response(status=200) + + +@router.callback_query(lambda c: c.data == "enter_custom_amount") +async def process_enter_custom_amount( + callback_query: types.CallbackQuery, state: FSMContext +): + await callback_query.message.edit_text(text="Введите сумму пополнения:") + await state.set_state(ReplenishBalanceState.entering_custom_amount) + await callback_query.answer() + + +@router.message(State(ReplenishBalanceState.entering_custom_amount)) +async def process_custom_amount_input(message: types.Message, state: FSMContext): + if message.text.isdigit(): + amount = int(message.text) + if amount <= 0: + await message.answer( + "Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:" + ) + return + + await state.update_data(amount=amount) + await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation) + + try: + payment = Payment.create( + { + "amount": {"value": str(amount), "currency": "RUB"}, + "confirmation": { + "type": "redirect", + "return_url": "https://pocomacho.ru/", + }, + "capture": True, + "description": "Пополнение баланса", + "receipt": { + "customer": { + "full_name": message.from_user.full_name, + "email": f"{message.from_user.id}@solo.net", + "phone": "79000000000", + }, + "items": [ + { + "description": "Пополнение баланса", + "quantity": "1.00", + "amount": {"value": str(amount), "currency": "RUB"}, + "vat_code": 6, + } + ], + }, + "metadata": {"user_id": message.from_user.id}, + }, + uuid.uuid4(), + ) + + if payment["status"] == "pending": + payment_url = payment["confirmation"]["confirmation_url"] + + confirm_keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [InlineKeyboardButton(text="Пополнить", url=payment_url)], + [ + InlineKeyboardButton( + text="⬅️ Назад", callback_data="back_to_profile" + ) + ], + ] + ) + + await message.answer( + text=f"Вы выбрали пополнение на {amount} рублей.", + reply_markup=confirm_keyboard, + ) + else: + await message.answer("Ошибка при создании платежа.") + + except Exception as e: + logging.error(f"Ошибка при создании платежа: {e}") + await message.answer("Произошла ошибка при создании платежа.") + else: + await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:") diff --git a/handlers/profile.py b/handlers/profile.py index 36799b33..59a97274 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -4,30 +4,37 @@ import os from aiogram import Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup -from aiogram.types import (BufferedInputFile, InlineKeyboardButton, - InlineKeyboardMarkup) +from aiogram.types import BufferedInputFile, InlineKeyboardButton, InlineKeyboardMarkup from bot import bot from config import PAYMENT_METHOD from database import get_balance, get_key_count, get_referral_stats -from handlers.texts import (CHANNEL_LINK, get_referral_link, - invite_message_send, profile_message_send) +from handlers.texts import ( + CHANNEL_LINK, + get_referral_link, + invite_message_send, + profile_message_send, +) class ReplenishBalanceState(StatesGroup): choosing_transfer_method = State() waiting_for_admin_confirmation = State() + router = Router() logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -async def process_callback_view_profile(callback_query: types.CallbackQuery, state: FSMContext): - tg_id = callback_query.from_user.id - username = callback_query.from_user.full_name - image_path = os.path.join(os.path.dirname(__file__), 'pic.jpg') +async def process_callback_view_profile( + callback_query: types.CallbackQuery, state: FSMContext +): + tg_id = callback_query.from_user.id + username = callback_query.from_user.full_name + + image_path = os.path.join(os.path.dirname(__file__), "pic.jpg") if not os.path.isfile(image_path): await bot.send_message(tg_id, "Файл изображения не найден.") @@ -37,37 +44,62 @@ async def process_callback_view_profile(callback_query: types.CallbackQuery, sta key_count = await get_key_count(tg_id) balance = await get_balance(tg_id) if balance is None: - balance = 0 + balance = 0 profile_message = profile_message_send(username, tg_id, balance, key_count) - - profile_message += ( - f"Обязательно подпишитесь на канал здесь\n" - ) - + + profile_message += f"Обязательно подпишитесь на канал здесь\n" + if key_count == 0: - profile_message += "\nНажмите ➕Устройство снизу, чтобы добавить устройство в VPN" - - inline_keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='➕ Устройство', callback_data='create_key'), InlineKeyboardButton(text='📱 Мои устр-ва', callback_data='view_keys')], - [InlineKeyboardButton(text='💳 Пополнить баланс', callback_data='pay_freekassa' if PAYMENT_METHOD == 'freekassa' else 'replenish_balance')], - [InlineKeyboardButton(text='👥 Пригласить', callback_data='invite'), InlineKeyboardButton(text='📘 Инструкции', callback_data='instructions')], - [InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_menu')] - ]) + profile_message += ( + "\nНажмите ➕Устройство снизу, чтобы добавить устройство в VPN" + ) + + inline_keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton( + text="➕ Устройство", callback_data="create_key" + ), + InlineKeyboardButton( + text="📱 Мои устр-ва", callback_data="view_keys" + ), + ], + [ + InlineKeyboardButton( + text="💳 Пополнить баланс", + callback_data=( + "pay_freekassa" + if PAYMENT_METHOD == "freekassa" + else "replenish_balance" + ), + ) + ], + [ + InlineKeyboardButton(text="👥 Пригласить", callback_data="invite"), + InlineKeyboardButton( + text="📘 Инструкции", callback_data="instructions" + ), + ], + [InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_menu")], + ] + ) # Попробуем удалить предыдущее сообщение try: await callback_query.message.delete() except Exception as e: - logger.error(f"Ошибка при удалении сообщения: {e}") # Логируем ошибку, если удаление не удалось + logger.error( + f"Ошибка при удалении сообщения: {e}" + ) # Логируем ошибку, если удаление не удалось - with open(image_path, 'rb') as image_file: + with open(image_path, "rb") as image_file: await bot.send_photo( chat_id=tg_id, photo=BufferedInputFile(image_file.read(), filename="pic.jpg"), caption=profile_message, - parse_mode='HTML', - reply_markup=inline_keyboard + parse_mode="HTML", + reply_markup=inline_keyboard, ) except Exception as e: @@ -75,31 +107,28 @@ async def process_callback_view_profile(callback_query: types.CallbackQuery, sta await callback_query.answer() -@router.callback_query(lambda c: c.data == 'invite') + +@router.callback_query(lambda c: c.data == "invite") async def invite_handler(callback_query: types.CallbackQuery): tg_id = callback_query.from_user.id referral_link = get_referral_link(tg_id) - + referral_stats = await get_referral_stats(tg_id) - - invite_message = ( - invite_message_send(referral_link,referral_stats) - ) - - button_back = InlineKeyboardButton(text='⬅️ Назад', callback_data='view_profile') + + invite_message = invite_message_send(referral_link, referral_stats) + + button_back = InlineKeyboardButton(text="⬅️ Назад", callback_data="view_profile") keyboard = InlineKeyboardMarkup(inline_keyboard=[[button_back]]) await callback_query.message.delete() await bot.send_message( - chat_id=tg_id, - text=invite_message, - parse_mode='HTML', - reply_markup=keyboard + chat_id=tg_id, text=invite_message, parse_mode="HTML", reply_markup=keyboard ) await callback_query.answer() -@router.callback_query(lambda c: c.data == 'view_profile') + +@router.callback_query(lambda c: c.data == "view_profile") async def view_profile_handler(callback_query: types.CallbackQuery, state: FSMContext): await process_callback_view_profile(callback_query, state) diff --git a/handlers/start.py b/handlers/start.py index 8422a8d0..532d9c7f 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -1,87 +1,113 @@ +import logging import os import asyncpg from aiogram import Router from aiogram.filters import Command from aiogram.fsm.state import State, StatesGroup -from aiogram.types import (BufferedInputFile, CallbackQuery, - InlineKeyboardButton, InlineKeyboardMarkup, Message) +from aiogram.types import ( + BufferedInputFile, + CallbackQuery, + InlineKeyboardButton, + InlineKeyboardMarkup, + Message, +) from bot import bot from config import APP_URL, CHANNEL_URL, DATABASE_URL, SUPPORT_CHAT_URL -from database import (add_connection, add_referral, check_connection_exists, - get_trial) +from database import add_connection, add_referral, check_connection_exists, get_trial from handlers.keys.trial_key import create_trial_key from handlers.texts import ABOUT_VPN, INSTRUCTIONS_TRIAL, WELCOME_TEXT +logging.basicConfig(level=logging.DEBUG) + router = Router() + class FeedbackState(StatesGroup): waiting_for_feedback = State() + async def send_welcome_message(chat_id: int, trial_status: int): welcome_text = WELCOME_TEXT - image_path = os.path.join(os.path.dirname(__file__), 'pic.jpg') + image_path = os.path.join(os.path.dirname(__file__), "pic.jpg") if not os.path.isfile(image_path): await bot.send_message(chat_id, "Файл изображения не найден.") return - inline_keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='🔗 Подключить VPN', callback_data='connect_vpn')] if trial_status == 0 else [], - [InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')], - [InlineKeyboardButton(text='🔒 О VPN', callback_data='about_vpn')], - [InlineKeyboardButton(text='📞 Поддержка', url=SUPPORT_CHAT_URL)], - [InlineKeyboardButton(text='📢 Наш канал', url=CHANNEL_URL)], - ]) + inline_keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + ( + [ + InlineKeyboardButton( + text="🔗 Подключить VPN", callback_data="connect_vpn" + ) + ] + if trial_status == 0 + else [] + ), + [InlineKeyboardButton(text="👤 Мой профиль", callback_data="view_profile")], + [InlineKeyboardButton(text="🔒 О VPN", callback_data="about_vpn")], + [InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL)], + [InlineKeyboardButton(text="📢 Наш канал", url=CHANNEL_URL)], + ] + ) - inline_keyboard.inline_keyboard = [row for row in inline_keyboard.inline_keyboard if row] + inline_keyboard.inline_keyboard = [ + row for row in inline_keyboard.inline_keyboard if row + ] - with open(image_path, 'rb') as image_from_buffer: + with open(image_path, "rb") as image_from_buffer: await bot.send_photo( chat_id, BufferedInputFile(image_from_buffer.read(), filename="pic.jpg"), caption=welcome_text, - parse_mode='HTML', - reply_markup=inline_keyboard + parse_mode="HTML", + reply_markup=inline_keyboard, ) -@router.message(Command('start')) + +@router.message(Command("start")) async def start_command(message: Message): - print(f"Received start command with text: {message.text}") - if 'referral_' in message.text: - referrer_tg_id = int(message.text.split('referral_')[1]) - print(f"Referral ID: {referrer_tg_id}") + logging.info(f"Received start command with text: {message.text}") + if "referral_" in message.text: + referrer_tg_id = int(message.text.split("referral_")[1]) + logging.info(f"Referral ID: {referrer_tg_id}") if not await check_connection_exists(message.from_user.id): await add_connection(message.from_user.id) await add_referral(message.from_user.id, referrer_tg_id) await message.answer("Вас пригласил друг, добро пожаловать!") else: await message.answer("Вы уже зарегистрированы в системе!") - + trial_status = await get_trial(message.from_user.id) await send_welcome_message(message.chat.id, trial_status) -@router.callback_query(lambda c: c.data == 'connect_vpn') + +@router.callback_query(lambda c: c.data == "connect_vpn") async def handle_connect_vpn(callback_query: CallbackQuery): await callback_query.message.delete() user_id = callback_query.from_user.id trial_key_info = await create_trial_key(user_id) - if 'error' in trial_key_info: - await callback_query.message.answer(trial_key_info['error']) + if "error" in trial_key_info: + await callback_query.message.answer(trial_key_info["error"]) else: conn = await asyncpg.connect(DATABASE_URL) try: - result = await conn.execute(''' + result = await conn.execute( + """ UPDATE connections SET trial = 1 WHERE tg_id = $1 - ''', user_id) - print(f"Rows updated: {result}") + """, + user_id, + ) + logging.info(f"Rows updated: {result}") except Exception as e: - print(f"Ошибка при обновлении trial: {e}") + logging.error(f"Ошибка при обновлении trial: {e}") finally: await conn.close() @@ -91,58 +117,59 @@ async def handle_connect_vpn(callback_query: CallbackQuery): f"Инструкции:\n{INSTRUCTIONS_TRIAL}" ) - button_profile = InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile') - + button_profile = InlineKeyboardButton( + text="👤 Мой профиль", callback_data="view_profile" + ) + button_iphone = InlineKeyboardButton( - text='🍏 Подключить', - url=f'{APP_URL}/?url=v2raytun://import/{trial_key_info["key"]}' + text="🍏 Подключить", + url=f'{APP_URL}/?url=v2raytun://import/{trial_key_info["key"]}', ) button_android = InlineKeyboardButton( - text='🤖 Подключить', - url=f'{APP_URL}/?url=v2raytun://import-sub?url={trial_key_info["key"]}' + text="🤖 Подключить", + url=f'{APP_URL}/?url=v2raytun://import-sub?url={trial_key_info["key"]}', ) button_download_iphone = InlineKeyboardButton( - text='🍏 Скачать', - url='https://apps.apple.com/ru/app/v2raytun/id6476628951' + text="🍏 Скачать", url="https://apps.apple.com/ru/app/v2raytun/id6476628951" ) button_download_android = InlineKeyboardButton( - text='🤖 Скачать', - url='https://play.google.com/store/apps/details?id=com.v2raytun.android&hl=ru' + text="🤖 Скачать", + url="https://play.google.com/store/apps/details?id=com.v2raytun.android&hl=ru", ) - inline_keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [button_download_iphone, button_download_android], - [button_iphone, button_android], - [button_profile] - ]) + inline_keyboard = InlineKeyboardMarkup( + inline_keyboard=[ + [button_download_iphone, button_download_android], + [button_iphone, button_android], + [button_profile], + ] + ) await callback_query.message.answer( - key_message, - parse_mode='HTML', - reply_markup=inline_keyboard + key_message, parse_mode="HTML", reply_markup=inline_keyboard ) await callback_query.answer() -@router.callback_query(lambda c: c.data == 'about_vpn') + +@router.callback_query(lambda c: c.data == "about_vpn") async def handle_about_vpn(callback_query: CallbackQuery): await callback_query.message.delete() - + bot_version = "3.0.1_beta" info_message = ABOUT_VPN.format(bot_version=bot_version) - button_back = InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_menu') + button_back = InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_menu") inline_keyboard_back = InlineKeyboardMarkup(inline_keyboard=[[button_back]]) await callback_query.message.answer( - info_message, - parse_mode='HTML', - reply_markup=inline_keyboard_back + info_message, parse_mode="HTML", reply_markup=inline_keyboard_back ) await callback_query.answer() -@router.callback_query(lambda c: c.data == 'back_to_menu') + +@router.callback_query(lambda c: c.data == "back_to_menu") async def handle_back_to_menu(callback_query: CallbackQuery): await callback_query.message.delete() trial_status = await get_trial(callback_query.from_user.id) diff --git a/handlers/utils.py b/handlers/utils.py index 3fd2506f..b3678a9f 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -5,23 +5,27 @@ from config import SERVERS def sanitize_key_name(key_name: str) -> str: - return re.sub(r'[^a-z0-9@._-]', '', key_name.lower()) + return re.sub(r"[^a-z0-9@._-]", "", key_name.lower()) + def generate_random_email(): """Генерирует случайный набор символов.""" - random_string = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=6)) - return random_string + random_string = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=6)) + return random_string + async def get_least_loaded_server(conn): """Находит сервер с наименьшей загрузкой.""" least_loaded_server_id = None - min_load_percentage = float('inf') + min_load_percentage = float("inf") for server_id, server in SERVERS.items(): - count = await conn.fetchval('SELECT COUNT(*) FROM keys WHERE server_id = $1', server_id) - percent_full = (count / 60) * 100 if count <= 60 else 100 + count = await conn.fetchval( + "SELECT COUNT(*) FROM keys WHERE server_id = $1", server_id + ) + percent_full = (count / 60) * 100 if count <= 60 else 100 if percent_full < min_load_percentage: min_load_percentage = percent_full least_loaded_server_id = server_id - return least_loaded_server_id \ No newline at end of file + return least_loaded_server_id diff --git a/middlewares/admin.py b/middlewares/admin.py new file mode 100644 index 00000000..92cb2212 --- /dev/null +++ b/middlewares/admin.py @@ -0,0 +1,57 @@ +from functools import wraps +from typing import Any, Awaitable, Callable, Dict + +from aiogram import BaseMiddleware +from aiogram.types import CallbackQuery, Message, TelegramObject + +from config import ADMIN_ID + + +class AdminMiddleware(BaseMiddleware): + async def __call__( + self, + handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + event: TelegramObject, + data: Dict[str, Any], + ) -> Any: + # Проверяем, является ли пользователь администратором + user_id = None + + if isinstance(event, Message): + user_id = event.from_user.id + elif isinstance(event, CallbackQuery): + user_id = event.from_user.id + + if user_id not in int(ADMIN_ID): + data["is_admin"] = False + else: + data["is_admin"] = True + + return await handler(event, data) + + +def admin_only(): + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + # Извлекаем объект события (Message или CallbackQuery) + event = args[0] + + # Определяем ID пользователя + user_id = event.from_user.id if hasattr(event, "from_user") else None + + if user_id not in int(ADMIN_ID): + # Можно отправить сообщение или просто return + if isinstance(event, Message): + await event.answer("У вас нет доступа к этой команде.") + elif isinstance(event, CallbackQuery): + await event.answer( + "У вас нет доступа к этому действию.", show_alert=True + ) + return None + + return await func(*args, **kwargs) + + return wrapper + + return decorator