From 76d72ba2039f32056c356cce9b3ac1dbc2068900 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Fri, 13 Dec 2024 12:09:20 +0300 Subject: [PATCH 1/2] Migrate to ruff --- .flake8 | 6 - Makefile | 9 +- backup.py | 11 +- bot.py | 2 +- client.py | 12 +- database.py | 191 ++++++++++++++------- filters/admin.py | 4 +- handlers/__init__.py | 5 +- handlers/admin/__init__.py | 4 +- handlers/admin/admin_coupons.py | 28 +++- handlers/admin/admin_panel.py | 132 +++++++++++---- handlers/admin/admin_servers.py | 198 ++++++++++++++++------ handlers/admin/admin_user_editor.py | 229 +++++++++++++++++++------- handlers/coupons.py | 8 +- handlers/donate.py | 24 ++- handlers/instructions/__init__.py | 4 +- handlers/instructions/instructions.py | 16 +- handlers/keys/__init__.py | 4 +- handlers/keys/key_management.py | 103 +++++++++--- handlers/keys/key_utils.py | 22 ++- handlers/keys/keys.py | 171 ++++++++++++++----- handlers/keys/subscriptions.py | 40 +++-- handlers/keys/trial_key.py | 4 +- handlers/notifications.py | 119 +++++++++---- handlers/pay.py | 19 ++- handlers/payments/__init__.py | 12 +- handlers/payments/cryprobot_pay.py | 50 ++++-- handlers/payments/freekassa_pay.py | 38 +++-- handlers/payments/robokassa_pay.py | 48 ++++-- handlers/payments/stars_pay.py | 38 +++-- handlers/payments/utils.py | 4 +- handlers/payments/yookassa_pay.py | 37 +++-- handlers/profile.py | 30 ++-- handlers/start.py | 58 +++++-- handlers/utils.py | 11 +- logger.py | 2 +- middlewares/admin.py | 10 +- middlewares/database.py | 10 +- middlewares/delete.py | 11 +- middlewares/logging.py | 9 +- middlewares/user.py | 7 +- pyproject.toml | 42 ++--- requirements.txt | 4 - servers.py | 62 +++++-- 44 files changed, 1328 insertions(+), 520 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index b51e9062..00000000 --- a/.flake8 +++ /dev/null @@ -1,6 +0,0 @@ -[flake8] -max-line-length = 120 -ignore = E203, E266, E501, W503, F541, E704, W293, W291, E126, E121, E123, E128, E302, E131, E231, W292, E402, E261, E305, E701 -max-complexity = 15 -select = B, C, E, F, W, T4, B9 -exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,.txt,config.py \ No newline at end of file diff --git a/Makefile b/Makefile index 2a739bd8..aef0b00f 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ formatting: - @echo "Running black..." && black . - @echo "Running isort..." && isort . - @echo "Running flake8..." && flake8 --config .flake8 - # @echo "Running pylint..." && pylint . + @echo "Running Ruff..." && ruff check . --fix + @echo "Running Ruff format..." && ruff format . + +lint: + @echo "Running Ruff checks..." && ruff check . \ No newline at end of file diff --git a/backup.py b/backup.py index ccee564c..329d7a22 100644 --- a/backup.py +++ b/backup.py @@ -1,11 +1,10 @@ -from datetime import datetime import os import subprocess -from typing import Union +from datetime import datetime from aiogram.types import BufferedInputFile - from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER + from logger import logger @@ -57,8 +56,10 @@ def _create_database_backup(): async def _send_backup_to_admin(bot, backup_file_path): try: with open(backup_file_path, "rb") as backup_file: - backup_input_file = BufferedInputFile(backup_file.read(), filename=os.path.basename(backup_file_path)) - admin_ids: Union[int, list[int]] = ADMIN_ID + backup_input_file = BufferedInputFile( + backup_file.read(), filename=os.path.basename(backup_file_path) + ) + admin_ids: int | list[int] = ADMIN_ID if isinstance(admin_ids, list): for id in admin_ids: await bot.send_document(id, backup_input_file) diff --git a/bot.py b/bot.py index 62c86a2a..8799cd3c 100644 --- a/bot.py +++ b/bot.py @@ -5,8 +5,8 @@ from aiogram.client.default import DefaultBotProperties from aiogram.enums import ParseMode from aiogram.fsm.storage.memory import MemoryStorage from aiogram.types import ErrorEvent - from config import API_TOKEN + from logger import logger from middlewares.admin import AdminMiddleware from middlewares.database import DatabaseMiddleware diff --git a/client.py b/client.py index 660214e0..76451786 100644 --- a/client.py +++ b/client.py @@ -44,7 +44,9 @@ async def add_client( return {"status": "failed", "error": str(e)} -async def extend_client_key(xui, inbound_id, email: str, new_expiry_time: int, client_id: str, total_gb: int): +async def extend_client_key( + xui, inbound_id, email: str, new_expiry_time: int, client_id: str, total_gb: int +): """ Функция для обновления срока действия ключа клиента по email. """ @@ -60,7 +62,9 @@ async def extend_client_key(xui, inbound_id, email: str, new_expiry_time: int, c logger.warning(f"Ошибка: клиент {email} не имеет действительного ID.") return - logger.info(f"Обновление ключа клиента {client.email} с ID {client.id} до нового времени: {new_expiry_time}") + logger.info( + f"Обновление ключа клиента {client.email} с ID {client.id} до нового времени: {new_expiry_time}" + ) client.id = client_id client.expiry_time = new_expiry_time @@ -73,7 +77,9 @@ async def extend_client_key(xui, inbound_id, email: str, new_expiry_time: int, c await xui.client.update(client.id, client) await xui.client.reset_stats(inbound_id, email) - logger.info(f"Ключ клиента {client.email} успешно продлён до {new_expiry_time}.") + logger.info( + f"Ключ клиента {client.email} успешно продлён до {new_expiry_time}." + ) except Exception as e: logger.error(f"Ошибка при обновлении клиента с email {email}: {e}") diff --git a/database.py b/database.py index a51ceb17..0472eac5 100644 --- a/database.py +++ b/database.py @@ -7,21 +7,21 @@ from config import DATABASE_URL, REFERRAL_BONUS_PERCENTAGES from logger import logger -async def init_db(file_path: str = 'assets/schema.sql'): - with open(file_path, 'r') as file: +async def init_db(file_path: str = "assets/schema.sql"): + with open(file_path) as file: sql_content = file.read() # Split the file content into individual SQL statements and connect to the database - statements = [stmt.strip() for stmt in sql_content.split(';') if stmt.strip()] + statements = [stmt.strip() for stmt in sql_content.split(";") if stmt.strip()] conn = await asyncpg.connect(DATABASE_URL) try: for statement in statements: await conn.execute(statement) except Exception as e: - logger.error(f'Error while executing SQL statement: {e}') + logger.error(f"Error while executing SQL statement: {e}") finally: - logger.info('Tables created successfully') + logger.info("Tables created successfully") await conn.close() @@ -34,14 +34,18 @@ async def check_unique_server_name(server_name: str) -> bool: """ conn = await asyncpg.connect(DATABASE_URL) - result = await conn.fetchrow("SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", server_name) + result = await conn.fetchrow( + "SELECT 1 FROM servers WHERE server_name = $1 LIMIT 1", server_name + ) await conn.close() return result is None -async def create_coupon(coupon_code: str, amount: float, usage_limit: int, session: Any): +async def create_coupon( + coupon_code: str, amount: float, usage_limit: int, session: Any +): """ Создает новый купон в базе данных. @@ -180,7 +184,9 @@ async def restore_trial(tg_id: int, session: Any): logger.info(f"Триальный период успешно восстановлен для пользователя {tg_id}") return True except Exception as e: - logger.error(f"Ошибка при восстановлении триального периода для пользователя {tg_id}: {e}") + logger.error( + f"Ошибка при восстановлении триального периода для пользователя {tg_id}: {e}" + ) return False @@ -212,7 +218,9 @@ async def use_trial(tg_id: int, session: Any): return False -async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0, session: Any = None): +async def add_connection( + tg_id: int, balance: float = 0.0, trial: int = 0, session: Any = None +): """ Добавляет новое подключение для пользователя в базу данных. @@ -239,7 +247,9 @@ async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0, sessi f"Успешно добавлено новое подключение для пользователя {tg_id} с балансом {balance} и статусом триала {trial}" ) except Exception as e: - logger.error(f"Не удалось добавить подключение для пользователя {tg_id}. Причина: {e}") + logger.error( + f"Не удалось добавить подключение для пользователя {tg_id}. Причина: {e}" + ) raise @@ -276,7 +286,15 @@ async def check_connection_exists(tg_id: int): await conn.close() -async def store_key(tg_id: int, client_id: str, email: str, expiry_time: int, key: str, server_id: str, session: Any): +async def store_key( + tg_id: int, + client_id: str, + email: str, + expiry_time: int, + key: str, + server_id: str, + session: Any, +): """ Сохраняет информацию о ключе в базу данных. @@ -305,7 +323,9 @@ async def store_key(tg_id: int, client_id: str, email: str, expiry_time: int, ke key, server_id, ) - logger.info(f"Ключ успешно сохранен для пользователя {tg_id} на сервере {server_id}") + logger.info( + f"Ключ успешно сохранен для пользователя {tg_id} на сервере {server_id}" + ) except Exception as e: logger.error(f"Ошибка при сохранении ключа для пользователя {tg_id}: {e}") raise @@ -371,10 +391,14 @@ async def get_keys_by_server(tg_id: int, server_id: str): tg_id, server_id, ) - logger.info(f"Успешно получено {len(records)} ключей для пользователя {tg_id} на сервере {server_id}") + logger.info( + f"Успешно получено {len(records)} ключей для пользователя {tg_id} на сервере {server_id}" + ) return records except Exception as e: - logger.error(f"Ошибка при получении ключей для пользователя {tg_id} на сервере {server_id}: {e}") + logger.error( + f"Ошибка при получении ключей для пользователя {tg_id} на сервере {server_id}: {e}" + ) raise finally: if conn: @@ -398,10 +422,14 @@ async def has_active_key(tg_id: int) -> bool: try: conn = await asyncpg.connect(DATABASE_URL) count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE tg_id = $1", tg_id) - logger.info(f"Проверка наличия ключей для пользователя {tg_id}. Найдено ключей: {count}") + logger.info( + f"Проверка наличия ключей для пользователя {tg_id}. Найдено ключей: {count}" + ) return count > 0 except Exception as e: - logger.error(f"Ошибка при проверке наличия ключей для пользователя {tg_id}: {e}") + logger.error( + f"Ошибка при проверке наличия ключей для пользователя {tg_id}: {e}" + ) raise finally: if conn: @@ -424,7 +452,9 @@ async def get_balance(tg_id: int) -> float: conn = None try: 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 + ) logger.info(f"Получен баланс для пользователя {tg_id}: {balance}") return balance if balance is not None else 0.0 except Exception as e: @@ -482,11 +512,15 @@ async def get_trial(tg_id: int, session: Any) -> int: int: Статус триала (0 - не использован, 1 - использован) """ try: - trial = await session.fetchval("SELECT trial FROM connections WHERE tg_id = $1", tg_id) + trial = await session.fetchval( + "SELECT trial FROM connections WHERE tg_id = $1", tg_id + ) logger.info(f"Получен статус триала для пользователя {tg_id}: {trial}") return trial if trial is not None else 0 except Exception as e: - logger.error(f"Ошибка при получении статуса триала для пользователя {tg_id}: {e}") + logger.error( + f"Ошибка при получении статуса триала для пользователя {tg_id}: {e}" + ) return 0 @@ -510,7 +544,9 @@ async def get_key_count(tg_id: int) -> int: logger.info(f"Получено количество ключей для пользователя {tg_id}: {count}") return count if count is not None else 0 except Exception as e: - logger.error(f"Ошибка при получении количества ключей для пользователя {tg_id}: {e}") + logger.error( + f"Ошибка при получении количества ключей для пользователя {tg_id}: {e}" + ) return 0 finally: if conn: @@ -550,7 +586,9 @@ async def add_referral(referred_tg_id: int, referrer_tg_id: int, session: Any): referred_tg_id, referrer_tg_id, ) - logger.info(f"Добавлена реферальная связь: приглашенный {referred_tg_id}, пригласивший {referrer_tg_id}") + logger.info( + f"Добавлена реферальная связь: приглашенный {referred_tg_id}, пригласивший {referrer_tg_id}" + ) except Exception as e: logger.error(f"Ошибка при добавлении реферала: {e}") raise @@ -584,7 +622,9 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float): for level in range(1, MAX_REFERRAL_LEVELS + 1): if current_tg_id in visited_tg_ids: - logger.warning(f"Обнаружен цикл в реферальной цепочке для пользователя {current_tg_id}. Прекращение.") + logger.warning( + f"Обнаружен цикл в реферальной цепочке для пользователя {current_tg_id}. Прекращение." + ) break visited_tg_ids.add(current_tg_id) @@ -601,26 +641,30 @@ async def handle_referral_on_balance_update(tg_id: int, amount: float): if not referral: break - referrer_tg_id = referral['referrer_tg_id'] - referral_chain.append({'tg_id': referrer_tg_id, 'level': level}) + referrer_tg_id = referral["referrer_tg_id"] + referral_chain.append({"tg_id": referrer_tg_id, "level": level}) current_tg_id = referrer_tg_id for referral in referral_chain: - referrer_tg_id = referral['tg_id'] - level = referral['level'] + referrer_tg_id = referral["tg_id"] + level = referral["level"] bonus_percent = REFERRAL_BONUS_PERCENTAGES.get(level, 0) bonus = amount * bonus_percent bonus = max(bonus, 0) if bonus > 0: - logger.info(f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id} на уровне {level}") + logger.info( + f"Начисление бонуса {bonus} рублей рефереру {referrer_tg_id} на уровне {level}" + ) await update_balance(referrer_tg_id, bonus) except Exception as e: - logger.error(f"Ошибка при обработке многоуровневой реферальной системы для {tg_id}: {e}") + logger.error( + f"Ошибка при обработке многоуровневой реферальной системы для {tg_id}: {e}" + ) finally: if conn: await conn.close() @@ -696,7 +740,10 @@ async def get_referral_stats(referrer_tg_id: int): # Преобразование результатов в словарь referrals_by_level = { - record['level']: {'total': record['level_count'], 'active': record['active_level_count']} + record["level"]: { + "total": record["level_count"], + "active": record["active_level_count"], + } for record in referrals_by_level_records } logger.debug(f"Получена статистика рефералов по уровням: {referrals_by_level}") @@ -714,7 +761,9 @@ async def get_referral_stats(referrer_tg_id: int): """, referrer_tg_id, ) - logger.debug(f"Получена общая сумма бонусов от рефералов: {total_referral_bonus}") + logger.debug( + f"Получена общая сумма бонусов от рефералов: {total_referral_bonus}" + ) return { "total_referrals": total_referrals, @@ -724,7 +773,9 @@ async def get_referral_stats(referrer_tg_id: int): } except Exception as e: - logger.error(f"Ошибка при получении статистики рефералов для пользователя {referrer_tg_id}: {e}") + logger.error( + f"Ошибка при получении статистики рефералов для пользователя {referrer_tg_id}: {e}" + ) raise finally: if conn: @@ -746,7 +797,9 @@ async def update_key_expiry(client_id: str, new_expiry_time: int): conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для обновления времени истечения ключа клиента {client_id}") + logger.info( + f"Установлено подключение к базе данных для обновления времени истечения ключа клиента {client_id}" + ) await conn.execute( """ @@ -760,7 +813,9 @@ async def update_key_expiry(client_id: str, new_expiry_time: int): logger.info(f"Успешно обновлено время истечения ключа для клиента {client_id}") except Exception as e: - logger.error(f"Ошибка при обновлении времени истечения ключа для клиента {client_id}: {e}") + logger.error( + f"Ошибка при обновлении времени истечения ключа для клиента {client_id}: {e}" + ) raise finally: if conn: @@ -781,7 +836,9 @@ async def delete_key(client_id: str): conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для удаления ключа клиента {client_id}") + logger.info( + f"Установлено подключение к базе данных для удаления ключа клиента {client_id}" + ) await conn.execute( """ @@ -815,7 +872,9 @@ async def add_balance_to_client(client_id: str, amount: float): conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для пополнения баланса клиента {client_id}") + logger.info( + f"Установлено подключение к базе данных для пополнения баланса клиента {client_id}" + ) await conn.execute( """ @@ -853,7 +912,9 @@ async def get_client_id_by_email(email: str): conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для поиска client_id по email: {email}") + logger.info( + f"Установлено подключение к базе данных для поиска client_id по email: {email}" + ) client_id = await conn.fetchval( """ @@ -894,9 +955,13 @@ async def get_tg_id_by_client_id(client_id: str): conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для поиска Telegram ID по client_id: {client_id}") + logger.info( + f"Установлено подключение к базе данных для поиска Telegram ID по client_id: {client_id}" + ) - result = await conn.fetchrow("SELECT tg_id FROM keys WHERE client_id = $1", client_id) + result = await conn.fetchrow( + "SELECT tg_id FROM keys WHERE client_id = $1", client_id + ) if result: logger.info(f"Найден Telegram ID для client_id: {client_id}") @@ -915,12 +980,12 @@ async def get_tg_id_by_client_id(client_id: str): async def upsert_user( - tg_id: int, - username: str = None, - first_name: str = None, - last_name: str = None, - language_code: str = None, - is_bot: bool = False, + tg_id: int, + username: str = None, + first_name: str = None, + last_name: str = None, + language_code: str = None, + is_bot: bool = False, ): """ Обновляет или вставляет информацию о пользователе в базу данных. @@ -939,7 +1004,9 @@ async def upsert_user( conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для обновления пользователя {tg_id}") + logger.info( + f"Установлено подключение к базе данных для обновления пользователя {tg_id}" + ) await conn.execute( """ @@ -986,7 +1053,9 @@ async def add_payment(tg_id: int, amount: float, payment_system: str): conn = None try: conn = await asyncpg.connect(DATABASE_URL) - logger.info(f"Установлено подключение к базе данных для добавления платежа пользователя {tg_id}") + logger.info( + f"Установлено подключение к базе данных для добавления платежа пользователя {tg_id}" + ) await conn.execute( """ @@ -997,7 +1066,9 @@ async def add_payment(tg_id: int, amount: float, payment_system: str): amount, payment_system, ) - logger.info(f"Успешно добавлен платеж для пользователя {tg_id} на сумму {amount}") + logger.info( + f"Успешно добавлен платеж для пользователя {tg_id} на сумму {amount}" + ) except Exception as e: logger.error(f"Ошибка при добавлении платежа для пользователя {tg_id}: {e}") raise @@ -1030,13 +1101,19 @@ async def add_notification(tg_id: int, notification_type: str, session: Any): tg_id, notification_type, ) - logger.info(f"Успешно добавлено уведомление типа {notification_type} для пользователя {tg_id}") + logger.info( + f"Успешно добавлено уведомление типа {notification_type} для пользователя {tg_id}" + ) except Exception as e: - logger.error(f"Ошибка при добавлении notification для пользователя {tg_id}: {e}") + logger.error( + f"Ошибка при добавлении notification для пользователя {tg_id}: {e}" + ) raise -async def check_notification_time(tg_id: int, notification_type: str, hours: int = 12, session: Any = None) -> bool: +async def check_notification_time( + tg_id: int, notification_type: str, hours: int = 12, session: Any = None +) -> bool: """ Проверяет, прошло ли указанное количество часов с момента последнего уведомления. @@ -1081,7 +1158,9 @@ async def check_notification_time(tg_id: int, notification_type: str, hours: int return can_notify except Exception as e: - logger.error(f"Ошибка при проверке времени уведомления для пользователя {tg_id}: {e}") + logger.error( + f"Ошибка при проверке времени уведомления для пользователя {tg_id}: {e}" + ) return False finally: @@ -1103,16 +1182,16 @@ async def get_servers_from_db(): servers = {} for row in result: - cluster_name = row['cluster_name'] + cluster_name = row["cluster_name"] if cluster_name not in servers: servers[cluster_name] = [] servers[cluster_name].append( { - 'server_name': row['server_name'], - 'api_url': row['api_url'], - 'subscription_url': row['subscription_url'], - 'inbound_id': row['inbound_id'], + "server_name": row["server_name"], + "api_url": row["api_url"], + "subscription_url": row["subscription_url"], + "inbound_id": row["inbound_id"], } ) diff --git a/filters/admin.py b/filters/admin.py index b902294b..d940b31f 100644 --- a/filters/admin.py +++ b/filters/admin.py @@ -1,15 +1,13 @@ -from typing import Union from aiogram.filters import BaseFilter from aiogram.types import Message - from config import ADMIN_ID class IsAdminFilter(BaseFilter): async def __call__(self, message: Message) -> bool: try: - admin_ids: Union[int, list[int]] = ADMIN_ID + admin_ids: int | list[int] = ADMIN_ID if isinstance(admin_ids, list): return message.from_user.id in admin_ids return message.from_user.id == admin_ids diff --git a/handlers/__init__.py b/handlers/__init__.py index 613d08f9..f984092c 100644 --- a/handlers/__init__.py +++ b/handlers/__init__.py @@ -1,4 +1,4 @@ -__all__ = ('router',) +__all__ = ("router",) from aiogram import Router @@ -13,7 +13,7 @@ from .payments import router as payments_router from .profile import router as profile_router from .start import router as start_router -router = Router(name='handlers_main_router') +router = Router(name="handlers_main_router") router.include_routers( start_router, @@ -22,7 +22,6 @@ router.include_routers( donate_router, coupons_router, notifications_router, - payments_router, keys_router, instructions_router, diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py index 8f83973d..39109027 100644 --- a/handlers/admin/__init__.py +++ b/handlers/admin/__init__.py @@ -1,4 +1,4 @@ -__all__ = ('router',) +__all__ = ("router",) from aiogram import Router @@ -7,7 +7,7 @@ from .admin_panel import router as panel_router from .admin_servers import router as servers_router from .admin_user_editor import router as user_editor_router -router = Router(name='admins_main_router') +router = Router(name="admins_main_router") router.include_routers( panel_router, diff --git a/handlers/admin/admin_coupons.py b/handlers/admin/admin_coupons.py index 5ea3ca98..27e3071e 100644 --- a/handlers/admin/admin_coupons.py +++ b/handlers/admin/admin_coupons.py @@ -19,13 +19,19 @@ router = Router() @router.callback_query(F.data == "coupons_editor", IsAdminFilter()) -async def show_coupon_management_menu(callback_query: types.CallbackQuery, state: FSMContext): +async def show_coupon_management_menu( + callback_query: types.CallbackQuery, state: FSMContext +): await state.clear() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="➕ Создать купон", callback_data="create_coupon")) + builder.row( + InlineKeyboardButton(text="➕ Создать купон", callback_data="create_coupon") + ) builder.row(InlineKeyboardButton(text="Купоны", callback_data="coupons")) builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) - await callback_query.message.answer("🛠 Меню управления купонами:", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "🛠 Меню управления купонами:", reply_markup=builder.as_markup() + ) @router.callback_query(F.data == "coupons", IsAdminFilter()) @@ -35,7 +41,9 @@ async def show_coupon_list(callback_query: types.CallbackQuery, session: Any): if not coupons: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) + builder.row( + InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor") + ) await callback_query.message.answer( "❌ На данный момент нет доступных купонов. 🚫\nВы можете вернуться в меню управления. 🔙", @@ -61,8 +69,12 @@ async def show_coupon_list(callback_query: types.CallbackQuery, session: Any): ) ) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) - await callback_query.message.answer(coupon_list, reply_markup=builder.as_markup()) + builder.row( + InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor") + ) + await callback_query.message.answer( + coupon_list, reply_markup=builder.as_markup() + ) except Exception as e: logger.error(f"Ошибка при получении списка купонов: {e}") @@ -141,7 +153,9 @@ async def process_coupon_data(message: types.Message, state: FSMContext, session ) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")) + builder.row( + InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor") + ) await message.answer(result_message, reply_markup=builder.as_markup()) await state.clear() diff --git a/handlers/admin/admin_panel.py b/handlers/admin/admin_panel.py index b0adf30a..47865ad8 100644 --- a/handlers/admin/admin_panel.py +++ b/handlers/admin/admin_panel.py @@ -1,6 +1,6 @@ +import subprocess from datetime import datetime from io import BytesIO -import subprocess from typing import Any from aiogram import F, Router, types @@ -35,13 +35,35 @@ async def handle_admin_message(message: types.Message, state: FSMContext): await state.clear() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="📊 Статистика пользователей", callback_data="user_stats")) - builder.row(InlineKeyboardButton(text="👥 Управление пользователями", callback_data="user_editor")) - builder.row(InlineKeyboardButton(text="🖥️ Управление серверами", callback_data="servers_editor")) - builder.row(InlineKeyboardButton(text="🎟️ Управление купонами", callback_data="coupons_editor")) - builder.row(InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to_alls")) - builder.row(InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups")) - builder.row(InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot")) + builder.row( + InlineKeyboardButton( + text="📊 Статистика пользователей", callback_data="user_stats" + ) + ) + builder.row( + InlineKeyboardButton( + text="👥 Управление пользователями", callback_data="user_editor" + ) + ) + builder.row( + InlineKeyboardButton( + text="🖥️ Управление серверами", callback_data="servers_editor" + ) + ) + builder.row( + InlineKeyboardButton( + text="🎟️ Управление купонами", callback_data="coupons_editor" + ) + ) + builder.row( + InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to_alls") + ) + builder.row( + InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups") + ) + builder.row( + InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot") + ) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await message.answer("🤖 Панель администратора", reply_markup=builder.as_markup()) @@ -59,7 +81,9 @@ async def user_stats_menu(callback_query: CallbackQuery, session: Any): total_payments_week = await session.fetchval( "SELECT COALESCE(SUM(amount), 0) FROM payments WHERE created_at >= date_trunc('week', CURRENT_DATE)" ) - total_payments_all_time = await session.fetchval("SELECT COALESCE(SUM(amount), 0) FROM payments") + total_payments_all_time = await session.fetchval( + "SELECT COALESCE(SUM(amount), 0) FROM payments" + ) active_keys = await session.fetchval( "SELECT COUNT(*) FROM keys WHERE expiry_time > $1", @@ -83,12 +107,27 @@ async def user_stats_menu(callback_query: CallbackQuery, session: Any): ) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔄 Обновить", callback_data="user_stats")) - builder.row(InlineKeyboardButton(text="📥 Выгрузить пользователей в CSV", callback_data="export_users_csv")) - builder.row(InlineKeyboardButton(text="📥 Выгрузить оплаты в CSV", callback_data="export_payments_csv")) - builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin")) + builder.row( + InlineKeyboardButton(text="🔄 Обновить", callback_data="user_stats") + ) + builder.row( + InlineKeyboardButton( + text="📥 Выгрузить пользователей в CSV", + callback_data="export_users_csv", + ) + ) + builder.row( + InlineKeyboardButton( + text="📥 Выгрузить оплаты в CSV", callback_data="export_payments_csv" + ) + ) + builder.row( + InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin") + ) - await callback_query.message.answer(stats_message, reply_markup=builder.as_markup()) + await callback_query.message.answer( + stats_message, reply_markup=builder.as_markup() + ) except Exception as e: logger.error(f"Error in user_stats_menu: {e}") @@ -115,7 +154,9 @@ async def export_users_csv(callback_query: CallbackQuery, session: Any): ) if not users: - await callback_query.message.answer("📭 Нет пользователей для экспорта.", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "📭 Нет пользователей для экспорта.", reply_markup=builder.as_markup() + ) return csv_data = "tg_id,username,first_name,last_name,language_code,is_bot,balance,trial\n" # Заголовки CSV @@ -128,14 +169,17 @@ async def export_users_csv(callback_query: CallbackQuery, session: Any): file = BufferedInputFile(file_name.getvalue(), filename="users_export.csv") await callback_query.message.answer_document( - file, caption="📥 Экспорт пользователей в CSV", reply_markup=builder.as_markup() + file, + caption="📥 Экспорт пользователей в CSV", + reply_markup=builder.as_markup(), ) file_name.close() except Exception as e: logger.error(f"Ошибка при экспорте пользователей в CSV: {e}") await callback_query.message.answer( - "❗ Произошла ошибка при экспорте пользователей.", reply_markup=builder.as_markup() + "❗ Произошла ошибка при экспорте пользователей.", + reply_markup=builder.as_markup(), ) @@ -161,7 +205,9 @@ async def export_payments_csv(callback_query: CallbackQuery, session: Any): ) if not payments: - await callback_query.message.answer("📭 Нет платежей для экспорта.", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "📭 Нет платежей для экспорта.", reply_markup=builder.as_markup() + ) return csv_data = "tg_id,username,first_name,last_name,amount,payment_system,status,created_at\n" # Заголовки CSV @@ -181,7 +227,8 @@ async def export_payments_csv(callback_query: CallbackQuery, session: Any): except Exception as e: logger.error(f"Ошибка при экспорте платежей в CSV: {e}") await callback_query.message.answer( - "❗ Произошла ошибка при экспорте платежей.", reply_markup=builder.as_markup() + "❗ Произошла ошибка при экспорте платежей.", + reply_markup=builder.as_markup(), ) @@ -197,7 +244,9 @@ async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext): @router.message(UserEditorState.waiting_for_message, IsAdminFilter()) -async def process_message_to_all(message: types.Message, state: FSMContext, session: Any): +async def process_message_to_all( + message: types.Message, state: FSMContext, session: Any +): text_message = message.text try: @@ -214,7 +263,9 @@ async def process_message_to_all(message: types.Message, state: FSMContext, sess success_count += 1 except Exception as e: error_count += 1 - logger.error(f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}") + logger.error( + f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}" + ) await message.answer( f"📤 Рассылка завершена:\n" @@ -230,9 +281,13 @@ async def process_message_to_all(message: types.Message, state: FSMContext, sess @router.callback_query(F.data == "backups", IsAdminFilter()) async def handle_backup(callback_query: CallbackQuery, state: FSMContext): - await callback_query.message.answer("💾 Инициализация резервного копирования базы данных...") + await callback_query.message.answer( + "💾 Инициализация резервного копирования базы данных..." + ) await backup_database() - await callback_query.message.answer("✅ Резервная копия успешно создана и отправлена администратору.") + await callback_query.message.answer( + "✅ Резервная копия успешно создана и отправлена администратору." + ) @router.callback_query(F.data == "restart_bot", IsAdminFilter()) @@ -240,7 +295,9 @@ async def handle_restart(callback_query: CallbackQuery, state: FSMContext): await state.set_state(UserEditorState.waiting_for_restart_confirmation) builder = InlineKeyboardBuilder() builder.row( - InlineKeyboardButton(text="✅ Да, перезапустить", callback_data="confirm_restart"), + InlineKeyboardButton( + text="✅ Да, перезапустить", callback_data="confirm_restart" + ), InlineKeyboardButton(text="❌ Нет, отмена", callback_data="admin"), ) builder.row(InlineKeyboardButton(text="🔙 Вернуться в меню", callback_data="admin")) @@ -266,12 +323,17 @@ async def confirm_restart_bot(callback_query: CallbackQuery, state: FSMContext): text=True, ) await state.clear() - await callback_query.message.answer("🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup() + ) except subprocess.CalledProcessError: - await callback_query.message.answer("🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "🔄 Бот успешно перезапущен.", reply_markup=builder.as_markup() + ) except Exception as e: await callback_query.message.answer( - f"⚠️ Ошибка при перезагрузке бота: {e.stderr}", reply_markup=builder.as_markup() + f"⚠️ Ошибка при перезагрузке бота: {e.stderr}", + reply_markup=builder.as_markup(), ) @@ -284,7 +346,17 @@ async def user_editor_menu(callback_query: CallbackQuery): callback_data="search_by_key_name", ) ) - builder.row(InlineKeyboardButton(text="🆔 Поиск по Telegram ID", callback_data="search_by_tg_id")) - builder.row(InlineKeyboardButton(text="🌐 Поиск по Username", callback_data="search_by_username")) + builder.row( + InlineKeyboardButton( + text="🆔 Поиск по Telegram ID", callback_data="search_by_tg_id" + ) + ) + builder.row( + InlineKeyboardButton( + text="🌐 Поиск по Username", callback_data="search_by_username" + ) + ) builder.row(InlineKeyboardButton(text="🔙 Вернуться назад", callback_data="admin")) - await callback_query.message.answer("👇 Выберите способ поиска пользователя:", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "👇 Выберите способ поиска пользователя:", reply_markup=builder.as_markup() + ) diff --git a/handlers/admin/admin_servers.py b/handlers/admin/admin_servers.py index b9260c66..8a030119 100644 --- a/handlers/admin/admin_servers.py +++ b/handlers/admin/admin_servers.py @@ -1,13 +1,13 @@ +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 InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -import asyncpg +from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL from py3xui import AsyncApi from backup import create_backup_and_send_to_admins -from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL from database import check_unique_server_name, get_servers_from_db from filters.admin import IsAdminFilter @@ -29,9 +29,15 @@ async def handle_servers_editor(callback_query: types.CallbackQuery): builder = InlineKeyboardBuilder() for cluster_name, cluster_servers in servers.items(): - builder.row(InlineKeyboardButton(text=f"⚙️ {cluster_name}", callback_data=f"manage_cluster|{cluster_name}")) + builder.row( + InlineKeyboardButton( + text=f"⚙️ {cluster_name}", callback_data=f"manage_cluster|{cluster_name}" + ) + ) - builder.row(InlineKeyboardButton(text="➕ Добавить кластер", callback_data="add_cluster")) + builder.row( + InlineKeyboardButton(text="➕ Добавить кластер", callback_data="add_cluster") + ) builder.row(InlineKeyboardButton(text="🔙 Назад в админку", callback_data="admin")) await callback_query.message.answer( @@ -64,7 +70,11 @@ async def handle_cluster_name_input(message: types.Message, state: FSMContext): if cluster_name == "❌ Отменить": await state.clear() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton( + text="🔧 Управление кластерами", callback_data="servers_editor" + ) + ) await message.answer( "Процесс создания кластера отменен. Вы вернулись в меню управления серверами.", reply_markup=builder.as_markup(), @@ -78,7 +88,9 @@ async def handle_cluster_name_input(message: types.Message, state: FSMContext): await state.update_data(cluster_name=cluster_name) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") + ) await message.answer( f"Введите имя сервера для кластера {cluster_name}:\n\n" @@ -97,7 +109,11 @@ async def handle_server_name_input(message: types.Message, state: FSMContext): if server_name == "❌ Отменить": await state.clear() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton( + text="🔧 Управление кластерами", callback_data="servers_editor" + ) + ) await message.answer( "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", reply_markup=builder.as_markup(), @@ -110,15 +126,19 @@ async def handle_server_name_input(message: types.Message, state: FSMContext): server_unique = await check_unique_server_name(server_name) if not server_unique: - await message.answer("❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя.") + await message.answer( + "❌ Сервер с таким именем уже существует. Пожалуйста, выберите другое имя." + ) return user_data = await state.get_data() - cluster_name = user_data.get('cluster_name') + cluster_name = user_data.get("cluster_name") await state.update_data(server_name=server_name) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") + ) await message.answer( f"Введите API URL для сервера {server_name} в кластере {cluster_name}:\n\n" @@ -138,7 +158,11 @@ async def handle_api_url_input(message: types.Message, state: FSMContext): if api_url == "❌ Отменить": await state.clear() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton( + text="🔧 Управление кластерами", callback_data="servers_editor" + ) + ) await message.answer( "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", reply_markup=builder.as_markup(), @@ -152,15 +176,17 @@ async def handle_api_url_input(message: types.Message, state: FSMContext): ) return - api_url = api_url.rstrip('/') + api_url = api_url.rstrip("/") user_data = await state.get_data() - cluster_name = user_data.get('cluster_name') - server_name = user_data.get('server_name') + cluster_name = user_data.get("cluster_name") + server_name = user_data.get("server_name") await state.update_data(api_url=api_url) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") + ) await message.answer( f"Введите subscription_url для сервера {server_name} в кластере {cluster_name}:\n\n" @@ -181,7 +207,11 @@ async def handle_subscription_url_input(message: types.Message, state: FSMContex if subscription_url == "❌ Отменить": await state.clear() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔧 Управление кластерами", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton( + text="🔧 Управление кластерами", callback_data="servers_editor" + ) + ) await message.answer( "Процесс создания кластера был отменен. Вы вернулись в меню управления серверами.", reply_markup=builder.as_markup(), @@ -195,15 +225,17 @@ async def handle_subscription_url_input(message: types.Message, state: FSMContex ) return - subscription_url = subscription_url.rstrip('/') + subscription_url = subscription_url.rstrip("/") user_data = await state.get_data() - cluster_name = user_data.get('cluster_name') - server_name = user_data.get('server_name') + cluster_name = user_data.get("cluster_name") + server_name = user_data.get("server_name") await state.update_data(subscription_url=subscription_url) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") + ) await message.answer( f"Введите inbound_id для сервера {server_name} в кластере {cluster_name}:\n\n" @@ -219,14 +251,16 @@ async def handle_inbound_id_input(message: types.Message, state: FSMContext): inbound_id = message.text.strip() if not inbound_id.isdigit(): - await message.answer("❌ inbound_id должен быть числовым значением. Попробуйте снова.") + await message.answer( + "❌ inbound_id должен быть числовым значением. Попробуйте снова." + ) return user_data = await state.get_data() - cluster_name = user_data.get('cluster_name') - server_name = user_data.get('server_name') - api_url = user_data.get('api_url') - subscription_url = user_data.get('subscription_url') + cluster_name = user_data.get("cluster_name") + server_name = user_data.get("server_name") + api_url = user_data.get("api_url") + subscription_url = user_data.get("subscription_url") conn = await asyncpg.connect(DATABASE_URL) await conn.execute( @@ -243,10 +277,15 @@ async def handle_inbound_id_input(message: types.Message, state: FSMContext): await conn.close() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад к кластерам", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton( + text="🔙 Назад к кластерам", callback_data="servers_editor" + ) + ) await message.answer( - f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!", reply_markup=builder.as_markup() + f"✅ Кластер {cluster_name} и сервер {server_name} успешно добавлены!", + reply_markup=builder.as_markup(), ) await state.clear() @@ -264,22 +303,40 @@ async def handle_manage_cluster(callback_query: types.CallbackQuery, state: FSMC for server in cluster_servers: builder.row( InlineKeyboardButton( - text=f"🌍 {server['server_name']}", callback_data=f"manage_server|{server['server_name']}" + text=f"🌍 {server['server_name']}", + callback_data=f"manage_server|{server['server_name']}", ) ) - builder.row(InlineKeyboardButton(text="➕ Добавить сервер", callback_data=f"add_server|{cluster_name}")) - builder.row( - InlineKeyboardButton(text="🌐 Доступность серверов", callback_data=f"server_availability|{cluster_name}") + InlineKeyboardButton( + text="➕ Добавить сервер", callback_data=f"add_server|{cluster_name}" + ) ) - builder.row(InlineKeyboardButton(text="💾 Создать бэкап кластера", callback_data=f"backup_cluster|{cluster_name}")) + builder.row( + InlineKeyboardButton( + text="🌐 Доступность серверов", + callback_data=f"server_availability|{cluster_name}", + ) + ) - builder.row(InlineKeyboardButton(text="🔙 Назад в управление кластерами", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton( + text="💾 Создать бэкап кластера", + callback_data=f"backup_cluster|{cluster_name}", + ) + ) + + builder.row( + InlineKeyboardButton( + text="🔙 Назад в управление кластерами", callback_data="servers_editor" + ) + ) await callback_query.message.answer( - f"🔧 Управление серверами для кластера {cluster_name}", reply_markup=builder.as_markup() + f"🔧 Управление серверами для кластера {cluster_name}", + reply_markup=builder.as_markup(), ) @@ -299,24 +356,36 @@ async def handle_check_server_availability(callback_query: types.CallbackQuery): "Это может занять до 1 минуты, пожалуйста, подождите..." ) - availability_message = f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n" + availability_message = ( + f"🖥️ Проверка доступности серверов для кластера {cluster_name} завершена:\n\n" + ) for server in cluster_servers: - xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD) + xui = AsyncApi( + server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD + ) try: await xui.login() online_users = len(await xui.client.online()) - availability_message += f"🌍 {server['server_name']}: {online_users} активных пользователей.\n" + availability_message += ( + f"🌍 {server['server_name']}: {online_users} активных пользователей.\n" + ) except Exception as e: availability_message += f"❌ {server['server_name']}: Не удалось получить информацию. Ошибка: {e}\n" builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}")) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}" + ) + ) - await in_progress_message.edit_text(availability_message, reply_markup=builder.as_markup()) + await in_progress_message.edit_text( + availability_message, reply_markup=builder.as_markup() + ) await callback_query.answer() @@ -330,19 +399,29 @@ async def handle_manage_server(callback_query: types.CallbackQuery, state: FSMCo server = None cluster_name = None for cluster, cluster_servers in servers.items(): - server = next((s for s in cluster_servers if s['server_name'] == server_name), None) + server = next( + (s for s in cluster_servers if s["server_name"] == server_name), None + ) if server: cluster_name = cluster break if server: - api_url = server['api_url'] - subscription_url = server['subscription_url'] - inbound_id = server['inbound_id'] + api_url = server["api_url"] + subscription_url = server["subscription_url"] + inbound_id = server["inbound_id"] builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🗑️ Удалить", callback_data=f"delete_server|{server_name}")) - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}")) + builder.row( + InlineKeyboardButton( + text="🗑️ Удалить", callback_data=f"delete_server|{server_name}" + ) + ) + builder.row( + InlineKeyboardButton( + text="🔙 Назад", callback_data=f"manage_cluster|{cluster_name}" + ) + ) await callback_query.message.answer( f"🔧 Информация о сервере {server_name}:\n\n" @@ -362,17 +441,24 @@ async def handle_delete_server(callback_query: types.CallbackQuery, state: FSMCo builder = InlineKeyboardBuilder() builder.row( - InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_delete_server|{server_name}"), - InlineKeyboardButton(text="❌ Нет", callback_data=f"manage_server|{server_name}"), + InlineKeyboardButton( + text="✅ Да", callback_data=f"confirm_delete_server|{server_name}" + ), + InlineKeyboardButton( + text="❌ Нет", callback_data=f"manage_server|{server_name}" + ), ) await callback_query.message.answer( - f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?", reply_markup=builder.as_markup() + f"🗑️ Вы уверены, что хотите удалить сервер {server_name}?", + reply_markup=builder.as_markup(), ) @router.callback_query(F.data.startswith("confirm_delete_server|"), IsAdminFilter()) -async def handle_confirm_delete_server(callback_query: types.CallbackQuery, state: FSMContext): +async def handle_confirm_delete_server( + callback_query: types.CallbackQuery, state: FSMContext +): server_name = callback_query.data.split("|")[1] conn = await asyncpg.connect(DATABASE_URL) @@ -385,9 +471,15 @@ async def handle_confirm_delete_server(callback_query: types.CallbackQuery, stat await conn.close() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад в управление кластерами", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton( + text="🔙 Назад в управление кластерами", callback_data="servers_editor" + ) + ) - await callback_query.message.answer(f"🗑️ Сервер {server_name} успешно удален.", reply_markup=builder.as_markup()) + await callback_query.message.answer( + f"🗑️ Сервер {server_name} успешно удален.", reply_markup=builder.as_markup() + ) @router.callback_query(F.data.startswith("add_server|"), IsAdminFilter()) @@ -397,7 +489,9 @@ async def handle_add_server(callback_query: types.CallbackQuery, state: FSMConte await state.update_data(cluster_name=cluster_name) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor")) + builder.row( + InlineKeyboardButton(text="❌ Отменить", callback_data="servers_editor") + ) await callback_query.message.answer( f"Введите имя сервера для кластера {cluster_name}:\n\n" diff --git a/handlers/admin/admin_user_editor.py b/handlers/admin/admin_user_editor.py index d579cb5f..8a89d875 100644 --- a/handlers/admin/admin_user_editor.py +++ b/handlers/admin/admin_user_editor.py @@ -7,11 +7,20 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import TOTAL_GB -from database import get_client_id_by_email, get_servers_from_db, restore_trial, update_key_expiry + +from database import ( + get_client_id_by_email, + get_servers_from_db, + restore_trial, + update_key_expiry, +) from filters.admin import IsAdminFilter -from handlers.keys.key_utils import delete_key_from_cluster, delete_key_from_db, renew_key_in_cluster +from handlers.keys.key_utils import ( + delete_key_from_cluster, + delete_key_from_db, + renew_key_in_cluster, +) from handlers.utils import sanitize_key_name from logger import logger @@ -31,7 +40,9 @@ class UserEditorState(StatesGroup): async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer("🔍 Введите Telegram ID клиента:", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "🔍 Введите Telegram ID клиента:", reply_markup=builder.as_markup() + ) await state.set_state(UserEditorState.waiting_for_tg_id) @@ -39,39 +50,59 @@ async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext): async def prompt_username(callback_query: CallbackQuery, state: FSMContext): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer("🔍 Введите Username клиента:", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "🔍 Введите Username клиента:", reply_markup=builder.as_markup() + ) await state.set_state(UserEditorState.waiting_for_username) @router.message(UserEditorState.waiting_for_username, IsAdminFilter()) -async def handle_username_input(message: types.Message, state: FSMContext, session: Any): +async def handle_username_input( + message: types.Message, state: FSMContext, session: Any +): username = message.text.strip().lstrip("@") - user_record = await session.fetchrow("SELECT tg_id FROM users WHERE username = $1", username) + user_record = await session.fetchrow( + "SELECT tg_id FROM users WHERE username = $1", username + ) if not user_record: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer("🔍 Пользователь с указанным username не найден. 🚫", reply_markup=builder.as_markup()) + await message.answer( + "🔍 Пользователь с указанным username не найден. 🚫", + reply_markup=builder.as_markup(), + ) await state.clear() return tg_id = user_record["tg_id"] - username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id) - balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) + username = await session.fetchval( + "SELECT username FROM users WHERE tg_id = $1", tg_id + ) + balance = await session.fetchval( + "SELECT balance FROM connections WHERE tg_id = $1", tg_id + ) key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) - referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) + referral_count = await session.fetchval( + "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id + ) if balance is None: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer("🚫 Пользователь с указанным tg_id не найден. 🔍", reply_markup=builder.as_markup()) + await message.answer( + "🚫 Пользователь с указанным tg_id не найден. 🔍", + reply_markup=builder.as_markup(), + ) await state.clear() return builder = InlineKeyboardBuilder() for (email,) in key_records: - builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")) + builder.row( + InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}") + ) builder.row( InlineKeyboardButton( @@ -104,22 +135,33 @@ async def handle_username_input(message: types.Message, state: FSMContext, sessi @router.message(UserEditorState.waiting_for_tg_id, F.text.isdigit(), IsAdminFilter()) async def handle_tg_id_input(message: types.Message, state: FSMContext, session: Any): tg_id = int(message.text) - username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id) - balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) + username = await session.fetchval( + "SELECT username FROM users WHERE tg_id = $1", tg_id + ) + balance = await session.fetchval( + "SELECT balance FROM connections WHERE tg_id = $1", tg_id + ) key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) - referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) + referral_count = await session.fetchval( + "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id + ) if balance is None: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer("❌ Пользователь с указанным tg_id не найден. 🔍", reply_markup=builder.as_markup()) + await message.answer( + "❌ Пользователь с указанным tg_id не найден. 🔍", + reply_markup=builder.as_markup(), + ) await state.clear() return builder = InlineKeyboardBuilder() for (email,) in key_records: - builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")) + builder.row( + InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}") + ) builder.row( InlineKeyboardButton( @@ -156,9 +198,15 @@ async def handle_restore_trial(callback_query: types.CallbackQuery, session: Any await restore_trial(tg_id, session) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад в меню администратора", callback_data="admin")) + builder.row( + InlineKeyboardButton( + text="🔙 Назад в меню администратора", callback_data="admin" + ) + ) - await callback_query.message.answer("✅ Триал успешно восстановлен.", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "✅ Триал успешно восстановлен.", reply_markup=builder.as_markup() + ) @router.callback_query(F.data.startswith("change_balance_"), IsAdminFilter()) @@ -167,17 +215,22 @@ async def process_balance_change(callback_query: CallbackQuery, state: FSMContex await state.update_data(tg_id=tg_id) builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer("💸 Введите новую сумму баланса:", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "💸 Введите новую сумму баланса:", reply_markup=builder.as_markup() + ) await state.set_state(UserEditorState.waiting_for_new_balance) @router.message(UserEditorState.waiting_for_new_balance, IsAdminFilter()) -async def handle_new_balance_input(message: types.Message, state: FSMContext, session: Any): +async def handle_new_balance_input( + message: types.Message, state: FSMContext, session: Any +): if not message.text.isdigit() or int(message.text) < 0: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) await message.answer( - "❌ Пожалуйста, введите корректную сумму для изменения баланса.", reply_markup=builder.as_markup() + "❌ Пожалуйста, введите корректную сумму для изменения баланса.", + reply_markup=builder.as_markup(), ) return @@ -222,11 +275,13 @@ async def get_key_details(email, session): cluster_name = "Неизвестный кластер" for cluster_name, cluster_servers in servers.items(): - if any(server['inbound_id'] == record['server_id'] for server in cluster_servers): + if any( + server["inbound_id"] == record["server_id"] for server in cluster_servers + ): cluster_name = cluster_name break - expiry_date = datetime.utcfromtimestamp(record['expiry_time'] / 1000) + expiry_date = datetime.utcfromtimestamp(record["expiry_time"] / 1000) current_date = datetime.utcnow() time_left = expiry_date - current_date @@ -239,12 +294,12 @@ async def get_key_details(email, session): days_left_message = f"Осталось часов: {hours_left}" return { - 'key': record['key'], - 'expiry_date': expiry_date.strftime("%d %B %Y года"), - 'days_left_message': days_left_message, - 'server_name': cluster_name, - 'balance': record['balance'], - 'tg_id': record['tg_id'], + "key": record["key"], + "expiry_date": expiry_date.strftime("%d %B %Y года"), + "days_left_message": days_left_message, + "server_name": cluster_name, + "balance": record["balance"], + "tg_id": record["tg_id"], } @@ -257,7 +312,8 @@ async def process_key_edit(callback_query: CallbackQuery, session: Any): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) await callback_query.message.answer( - "🔍 Информация о ключе не найдена. 🚫", reply_markup=builder.as_markup() + "🔍 Информация о ключе не найдена. 🚫", + reply_markup=builder.as_markup(), ) return @@ -289,19 +345,25 @@ async def process_key_edit(callback_query: CallbackQuery, session: Any): ) builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) - await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) + await callback_query.message.answer( + response_message, reply_markup=builder.as_markup() + ) @router.callback_query(F.data == "search_by_key_name", IsAdminFilter()) async def prompt_key_name(callback_query: CallbackQuery, state: FSMContext): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer("🔑 Введите имя ключа:", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "🔑 Введите имя ключа:", reply_markup=builder.as_markup() + ) await state.set_state(UserEditorState.waiting_for_key_name) @router.message(UserEditorState.waiting_for_key_name, IsAdminFilter()) -async def handle_key_name_input(message: types.Message, state: FSMContext, session: Any): +async def handle_key_name_input( + message: types.Message, state: FSMContext, session: Any +): key_name = sanitize_key_name(message.text) key_details = await get_key_details(key_name, session) @@ -358,34 +420,52 @@ async def prompt_expiry_change(callback_query: CallbackQuery, state: FSMContext) @router.message(UserEditorState.waiting_for_expiry_time, IsAdminFilter()) -async def handle_expiry_time_input(message: types.Message, state: FSMContext, session: Any): +async def handle_expiry_time_input( + message: types.Message, state: FSMContext, session: Any +): user_data = await state.get_data() email = user_data.get("email") if not email: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer("📧 Email не найден в состоянии. 🚫", reply_markup=builder.as_markup()) + await message.answer( + "📧 Email не найден в состоянии. 🚫", reply_markup=builder.as_markup() + ) await state.clear() return 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) if client_id is None: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer(f"🚫 Клиент с email {email} не найден. 🔍", reply_markup=builder.as_markup()) + builder.row( + InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor") + ) + await message.answer( + f"🚫 Клиент с email {email} не найден. 🔍", + reply_markup=builder.as_markup(), + ) await state.clear() return - record = await session.fetchrow("SELECT server_id FROM keys WHERE client_id = $1", client_id) + record = await session.fetchrow( + "SELECT server_id FROM keys WHERE client_id = $1", client_id + ) if not record: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await message.answer("🚫 Клиент не найден в базе данных. 🔍", reply_markup=builder.as_markup()) + builder.row( + InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor") + ) + await message.answer( + "🚫 Клиент не найден в базе данных. 🔍", + reply_markup=builder.as_markup(), + ) await state.clear() return @@ -412,9 +492,7 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se await update_key_expiry(client_id, expiry_time) - response_message = ( - f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах." - ) + response_message = f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах." builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin")) @@ -423,7 +501,8 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) await message.answer( - "❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.", reply_markup=builder.as_markup() + "❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.", + reply_markup=builder.as_markup(), ) except Exception as e: logger.error(e) @@ -431,14 +510,20 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext, se @router.callback_query(F.data.startswith("delete_key_admin|"), IsAdminFilter()) -async def process_callback_delete_key(callback_query: types.CallbackQuery, session: Any): +async def process_callback_delete_key( + callback_query: types.CallbackQuery, session: Any +): email = callback_query.data.split("|")[1] - client_id = await session.fetchval("SELECT client_id FROM keys WHERE email = $1", email) + client_id = await session.fetchval( + "SELECT client_id FROM keys WHERE email = $1", email + ) if client_id is None: builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")) - await callback_query.message.answer("🔍 Ключ не найден. 🚫", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "🔍 Ключ не найден. 🚫", reply_markup=builder.as_markup() + ) return builder = InlineKeyboardBuilder() @@ -448,7 +533,9 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery, sessi callback_data=f"confirm_delete_admin|{client_id}", ) ) - builder.row(types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="user_editor")) + builder.row( + types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="user_editor") + ) await callback_query.message.answer( "❓ Вы уверены, что хотите удалить ключ?", reply_markup=builder.as_markup(), @@ -456,9 +543,13 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery, sessi @router.callback_query(F.data.startswith("confirm_delete_admin|"), IsAdminFilter()) -async def process_callback_confirm_delete(callback_query: types.CallbackQuery, session: Any): +async def process_callback_confirm_delete( + callback_query: types.CallbackQuery, session: Any +): client_id = callback_query.data.split("|")[1] - record = await session.fetchrow("SELECT email FROM keys WHERE client_id = $1", client_id) + record = await session.fetchrow( + "SELECT email FROM keys WHERE client_id = $1", client_id + ) if record: email = record["email"] @@ -472,32 +563,48 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery, s tasks = [] for cluster_name, cluster_servers in clusters.items(): for server in cluster_servers: - tasks.append(delete_key_from_cluster(cluster_name, email, client_id)) + tasks.append( + delete_key_from_cluster(cluster_name, email, client_id) + ) await asyncio.gather(*tasks) await delete_key_from_servers(email, client_id) await delete_key_from_db(client_id, session) - await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) + await callback_query.message.answer( + response_message, reply_markup=builder.as_markup() + ) else: response_message = "🚫 Ключ не найден или уже удален." builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys")) - await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) + await callback_query.message.answer( + response_message, reply_markup=builder.as_markup() + ) @router.callback_query(F.data.startswith("user_info|"), IsAdminFilter()) -async def handle_user_info(callback_query: types.CallbackQuery, state: FSMContext, session: Any): +async def handle_user_info( + callback_query: types.CallbackQuery, state: FSMContext, session: Any +): tg_id = int(callback_query.data.split("|")[1]) - username = await session.fetchval("SELECT username FROM users WHERE tg_id = $1", tg_id) - balance = await session.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id) + username = await session.fetchval( + "SELECT username FROM users WHERE tg_id = $1", tg_id + ) + balance = await session.fetchval( + "SELECT balance FROM connections WHERE tg_id = $1", tg_id + ) key_records = await session.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id) - referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id) + referral_count = await session.fetchval( + "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id + ) builder = InlineKeyboardBuilder() for (email,) in key_records: - builder.row(InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")) + builder.row( + InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}") + ) builder.row( InlineKeyboardButton( diff --git a/handlers/coupons.py b/handlers/coupons.py index ed487671..15b3aa55 100644 --- a/handlers/coupons.py +++ b/handlers/coupons.py @@ -18,7 +18,9 @@ router = Router() @router.callback_query(F.data == "activate_coupon") -async def handle_activate_coupon(callback_query: types.CallbackQuery, state: FSMContext): +async def handle_activate_coupon( + callback_query: types.CallbackQuery, state: FSMContext +): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) @@ -38,7 +40,9 @@ async def process_coupon_code(message: types.Message, state: FSMContext, session builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - await message.answer(activation_result, reply_markup=builder.as_markup(), parse_mode="HTML") + await message.answer( + activation_result, reply_markup=builder.as_markup(), parse_mode="HTML" + ) await state.clear() diff --git a/handlers/donate.py b/handlers/donate.py index d441463a..6a37edc8 100644 --- a/handlers/donate.py +++ b/handlers/donate.py @@ -3,8 +3,8 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, LabeledPrice, PreCheckoutQuery from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import RUB_TO_XTR + from logger import logger @@ -22,7 +22,11 @@ async def process_donate(callback_query: types.CallbackQuery, state: FSMContext) await state.clear() builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot")) + builder.row( + InlineKeyboardButton( + text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot" + ) + ) builder.row( InlineKeyboardButton( text="💰 Ввести сумму доната", @@ -40,10 +44,14 @@ async def process_donate(callback_query: types.CallbackQuery, state: FSMContext) @router.callback_query(F.data == "enter_custom_donate_amount") -async def process_enter_donate_amount(callback_query: types.CallbackQuery, state: FSMContext): +async def process_enter_donate_amount( + callback_query: types.CallbackQuery, state: FSMContext +): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="donate")) - await callback_query.message.answer(f"💸 Введите сумму доната в рублях:", reply_markup=builder.as_markup()) + await callback_query.message.answer( + "💸 Введите сумму доната в рублях:", reply_markup=builder.as_markup() + ) await state.set_state(DonateState.entering_donate_amount) @@ -52,7 +60,9 @@ async def process_donate_amount_input(message: types.Message, state: FSMContext) if message.text.isdigit(): amount = int(message.text) if amount // RUB_TO_XTR <= 0: - await message.answer(f"Сумма доната должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:") + await message.answer( + f"Сумма доната должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:" + ) return await state.update_data(amount=amount) @@ -89,7 +99,9 @@ async def on_successful_donate(message: types.Message, state: FSMContext): try: amount = float(message.successful_payment.invoice_payload.split("_")[0]) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + ) await message.answer( text=f"🙏 Спасибо за донат {amount} рублей! Ваша поддержка очень важна для нас. 💖", reply_markup=builder.as_markup(), diff --git a/handlers/instructions/__init__.py b/handlers/instructions/__init__.py index b8121003..06350a7c 100644 --- a/handlers/instructions/__init__.py +++ b/handlers/instructions/__init__.py @@ -1,10 +1,10 @@ -__all__ = ('router',) +__all__ = ("router",) from aiogram import Router from .instructions import router as instructions_router -router = Router(name='instructions_main_router') +router = Router(name="instructions_main_router") router.include_routers( instructions_router, diff --git a/handlers/instructions/instructions.py b/handlers/instructions/instructions.py index 44d954c4..b8745d92 100644 --- a/handlers/instructions/instructions.py +++ b/handlers/instructions/instructions.py @@ -4,8 +4,8 @@ from typing import Any from aiogram import F, Router, types from aiogram.types import BufferedInputFile, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import CONNECT_IOS, CONNECT_WINDOWS, SUPPORT_CHAT_URL + from handlers.texts import INSTRUCTION_PC, INSTRUCTIONS, KEY_MESSAGE router = Router() @@ -49,7 +49,9 @@ async def process_connect_pc(callback_query: types.CallbackQuery, session: Any): ) if not record: - await callback_query.message.answer("❌ Ключ не найден. Проверьте имя ключа. 🔍") + await callback_query.message.answer( + "❌ Ключ не найден. Проверьте имя ключа. 🔍" + ) return key = record["key"] @@ -57,8 +59,14 @@ async def process_connect_pc(callback_query: types.CallbackQuery, session: Any): instruction_message = f"{key_message}{INSTRUCTION_PC}" builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="💻 Подключить Windows", url=f"{CONNECT_WINDOWS}{key}")) - builder.row(InlineKeyboardButton(text="💻 Подключить MacOS", url=f"{CONNECT_IOS}{key}")) + builder.row( + InlineKeyboardButton( + text="💻 Подключить Windows", url=f"{CONNECT_WINDOWS}{key}" + ) + ) + builder.row( + InlineKeyboardButton(text="💻 Подключить MacOS", url=f"{CONNECT_IOS}{key}") + ) builder.row(InlineKeyboardButton(text="🆘 Поддержка", url=f"{SUPPORT_CHAT_URL}")) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) diff --git a/handlers/keys/__init__.py b/handlers/keys/__init__.py index 45256dad..064024e6 100644 --- a/handlers/keys/__init__.py +++ b/handlers/keys/__init__.py @@ -1,11 +1,11 @@ -__all__ = ('router',) +__all__ = ("router",) from aiogram import Router from .key_management import router as management_router from .keys import router as keys_router -router = Router(name='keys_main_router') +router = Router(name="keys_main_router") router.include_routers( keys_router, diff --git a/handlers/keys/key_management.py b/handlers/keys/key_management.py index 9efe8ec1..1ef37a36 100644 --- a/handlers/keys/key_management.py +++ b/handlers/keys/key_management.py @@ -1,28 +1,27 @@ import asyncio -from datetime import datetime, timedelta -from typing import Any, Union import uuid +from datetime import datetime, timedelta +from typing import Any from aiogram import F, Router from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import CallbackQuery, InlineKeyboardButton, Message from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import ( CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, - RENEWAL_PLANS, RENEWAL_PRICES, SUPPORT_CHAT_URL, TRIAL_TIME, ) + from database import get_balance, get_trial, store_key, update_balance from handlers.keys.key_utils import create_key_on_cluster -from handlers.texts import DISCOUNTS, KEY, NULL_BALANCE, key_message_success +from handlers.texts import DISCOUNTS, KEY, key_message_success from handlers.utils import generate_random_email, get_least_loaded_cluster from logger import logger @@ -37,7 +36,9 @@ class Form(StatesGroup): @router.callback_query(F.data == "create_key") -async def process_callback_create_key(callback_query: CallbackQuery, state: FSMContext, session: Any): +async def process_callback_create_key( + callback_query: CallbackQuery, state: FSMContext, session: Any +): server_id = "все сервера" await state.update_data(selected_server_id=server_id) await select_server(callback_query, state, session) @@ -48,9 +49,14 @@ async def select_server(callback_query: CallbackQuery, state: FSMContext, sessio if trial_status == 1: builder = InlineKeyboardBuilder() builder.row( - InlineKeyboardButton(text="✅ Да, подключить новое устройство", callback_data="confirm_create_new_key") + InlineKeyboardButton( + text="✅ Да, подключить новое устройство", + callback_data="confirm_create_new_key", + ) + ) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") ) - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await callback_query.message.answer( text=KEY, @@ -58,22 +64,31 @@ async def select_server(callback_query: CallbackQuery, state: FSMContext, sessio ) await state.update_data(creating_new_key=True) else: - await handle_key_creation(callback_query.message.chat.id, state, session, callback_query) + await handle_key_creation( + callback_query.message.chat.id, state, session, callback_query + ) @router.callback_query(F.data == "confirm_create_new_key") -async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContext, session: Any): +async def confirm_create_new_key( + callback_query: CallbackQuery, state: FSMContext, session: Any +): tg_id = callback_query.message.chat.id logger.info(f"User {tg_id} confirmed creation of a new key.") - logger.info(f"Balance for user {tg_id} is sufficient. Proceeding with key creation.") + logger.info( + f"Balance for user {tg_id} is sufficient. Proceeding with key creation." + ) await handle_key_creation(tg_id, state, session, callback_query) async def handle_key_creation( - tg_id: int, state: FSMContext, session: Any, message_or_query: Union[Message, CallbackQuery] + tg_id: int, + state: FSMContext, + session: Any, + message_or_query: Message | CallbackQuery, ): """Создание ключа с учётом выбора тарифного плана.""" current_time = datetime.utcnow() @@ -83,7 +98,9 @@ async def handle_key_creation( expiry_time = current_time + timedelta(days=TRIAL_TIME) logger.info(f"Assigned 1-day trial to user {tg_id}.") - await session.execute("UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id) + await session.execute( + "UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id + ) await create_key(tg_id, expiry_time, state, session, message_or_query) else: builder = InlineKeyboardBuilder() @@ -100,21 +117,27 @@ async def handle_key_creation( builder.row( InlineKeyboardButton( - text=f"📅 {plan_id} мес. - {price}₽{discount_text}", callback_data=f"select_plan_{plan_id}" + text=f"📅 {plan_id} мес. - {price}₽{discount_text}", + callback_data=f"select_plan_{plan_id}", ) ) - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + ) await message_or_query.message.answer( - "💳 Выберите тарифный план для создания нового ключа:", reply_markup=builder.as_markup() + "💳 Выберите тарифный план для создания нового ключа:", + reply_markup=builder.as_markup(), ) await state.update_data(tg_id=tg_id) await state.set_state(Form.waiting_for_server_selection) @router.callback_query(F.data.startswith("select_plan_")) -async def select_tariff_plan(callback_query: CallbackQuery, state: FSMContext, session: Any): +async def select_tariff_plan( + callback_query: CallbackQuery, state: FSMContext, session: Any +): tg_id = callback_query.message.chat.id plan_id = callback_query.data.split("_")[-1] plan_price = RENEWAL_PRICES.get(plan_id) @@ -128,8 +151,12 @@ async def select_tariff_plan(callback_query: CallbackQuery, state: FSMContext, s balance = await get_balance(tg_id) if balance < plan_price: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay")) - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay") + ) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + ) await callback_query.message.answer( "💳 Недостаточно средств для создания подписки. Пополните баланс в личном кабинете.", reply_markup=builder.as_markup(), @@ -145,7 +172,11 @@ async def select_tariff_plan(callback_query: CallbackQuery, state: FSMContext, s async def create_key( - tg_id: int, expiry_time: datetime, state: FSMContext, session: Any, message_or_query: Union[Message, CallbackQuery] + tg_id: int, + expiry_time: datetime, + state: FSMContext, + session: Any, + message_or_query: Message | CallbackQuery, ): """Создаёт ключ с заданным сроком действия.""" while True: @@ -159,7 +190,9 @@ async def create_key( ) if not existing_key: break - logger.warning(f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one.") + logger.warning( + f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one." + ) client_id = str(uuid.uuid4()) email = key_name.lower() @@ -184,11 +217,21 @@ async def create_key( await asyncio.gather(*tasks) logger.info(f"Key created on cluster {least_loaded_cluster} for user {tg_id}.") - await store_key(tg_id, client_id, email, expiry_timestamp, public_link, least_loaded_cluster, session) + await store_key( + tg_id, + client_id, + email, + expiry_timestamp, + public_link, + least_loaded_cluster, + session, + ) except Exception as e: logger.error(f"Error while creating the key for user {tg_id} on cluster: {e}") - await message_or_query.message.answer("❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова.") + await message_or_query.message.answer( + "❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова." + ) return builder = InlineKeyboardBuilder() @@ -198,10 +241,18 @@ async def create_key( InlineKeyboardButton(text="🤖 Скачать для Android", url=DOWNLOAD_ANDROID), ) builder.row( - InlineKeyboardButton(text="🍏 Подключить на iOS", url=f"{CONNECT_IOS}{public_link}"), - InlineKeyboardButton(text="🤖 Подключить на Android", url=f"{CONNECT_ANDROID}{public_link}"), + InlineKeyboardButton( + text="🍏 Подключить на iOS", url=f"{CONNECT_IOS}{public_link}" + ), + InlineKeyboardButton( + text="🤖 Подключить на Android", url=f"{CONNECT_ANDROID}{public_link}" + ), + ) + builder.row( + InlineKeyboardButton( + text="💻 Windows/Linux", callback_data=f"connect_pc|{email}" + ) ) - builder.row(InlineKeyboardButton(text="💻 Windows/Linux", callback_data=f"connect_pc|{email}")) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) remaining_time = expiry_time - datetime.utcnow() diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py index 8d39c8b9..a66e1bec 100644 --- a/handlers/keys/key_utils.py +++ b/handlers/keys/key_utils.py @@ -1,10 +1,10 @@ import asyncio import asyncpg +from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, TOTAL_GB from py3xui import AsyncApi from client import add_client, delete_client, extend_client_key -from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, TOTAL_GB from database import get_servers_from_db from logger import logger @@ -33,7 +33,9 @@ async def create_key_on_cluster(cluster_id, tg_id, client_id, email, expiry_time continue conn = await asyncpg.connect(DATABASE_URL) - existing_key = await conn.fetchrow("SELECT 1 FROM keys WHERE email = $1", email) + existing_key = await conn.fetchrow( + "SELECT 1 FROM keys WHERE email = $1", email + ) if existing_key: raise ValueError(f"Email {email} уже существует в базе данных.") @@ -98,7 +100,9 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to await asyncio.gather(*tasks) except Exception as e: - logger.error(f"Не удалось продлить ключ {client_id} в кластере {cluster_id}: {e}") + logger.error( + f"Не удалось продлить ключ {client_id} в кластере {cluster_id}: {e}" + ) raise e @@ -145,7 +149,9 @@ async def delete_key_from_cluster(cluster_id, email, client_id): await asyncio.gather(*tasks) except Exception as e: - logger.error(f"Не удалось удалить ключ {client_id} в кластере {cluster_id}: {e}") + logger.error( + f"Не удалось удалить ключ {client_id} в кластере {cluster_id}: {e}" + ) raise e @@ -189,8 +195,12 @@ async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id await asyncio.gather(*tasks) - logger.info(f"Ключ успешно обновлен для {client_id} на всех серверах в кластере {cluster_id}") + logger.info( + f"Ключ успешно обновлен для {client_id} на всех серверах в кластере {cluster_id}" + ) except Exception as e: - logger.error(f"Ошибка при обновлении ключа на серверах кластера {cluster_id} для {client_id}: {e}") + logger.error( + f"Ошибка при обновлении ключа на серверах кластера {cluster_id} для {client_id}: {e}" + ) raise e diff --git a/handlers/keys/keys.py b/handlers/keys/keys.py index 236545b4..48d5e7d8 100644 --- a/handlers/keys/keys.py +++ b/handlers/keys/keys.py @@ -1,15 +1,30 @@ import asyncio -from datetime import datetime, timedelta import locale import os +from datetime import datetime, timedelta from typing import Any from aiogram import F, Router, types from aiogram.types import BufferedInputFile, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder +from config import ( + CONNECT_ANDROID, + CONNECT_IOS, + DOWNLOAD_ANDROID, + DOWNLOAD_IOS, + PUBLIC_LINK, + RENEWAL_PLANS, + TOTAL_GB, +) -from config import CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, RENEWAL_PLANS, TOTAL_GB -from database import delete_key, get_balance, get_servers_from_db, store_key, update_balance, update_key_expiry +from database import ( + delete_key, + get_balance, + get_servers_from_db, + store_key, + update_balance, + update_key_expiry, +) from handlers.keys.key_utils import ( delete_key_from_cluster, delete_key_from_db, @@ -48,20 +63,29 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery, sessio builder = InlineKeyboardBuilder() for record in records: key_name = record["email"] - builder.row(InlineKeyboardButton(text=f"🔑 {key_name}", callback_data=f"view_key|{key_name}")) + builder.row( + InlineKeyboardButton( + text=f"🔑 {key_name}", callback_data=f"view_key|{key_name}" + ) + ) - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + ) inline_keyboard = builder.as_markup() response_message = ( - "🔑 Список ваших устройств\n\n" "👇 Выберите устройство для управления подпиской:" + "🔑 Список ваших устройств\n\n" + "👇 Выберите устройство для управления подпиской:" ) image_path = os.path.join("img", "pic_keys.jpg") if os.path.isfile(image_path): with open(image_path, "rb") as image_file: await callback_query.message.answer_photo( - photo=BufferedInputFile(image_file.read(), filename="pic_keys.jpg"), + photo=BufferedInputFile( + image_file.read(), filename="pic_keys.jpg" + ), caption=response_message, reply_markup=inline_keyboard, ) @@ -74,8 +98,14 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery, sessio else: response_message = NO_KEYS builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="➕ Создать подписку", callback_data="create_key")) - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton( + text="➕ Создать подписку", callback_data="create_key" + ) + ) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + ) keyboard = builder.as_markup() @@ -84,7 +114,9 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery, sessio if os.path.isfile(image_path): with open(image_path, "rb") as image_file: await callback_query.message.answer_photo( - photo=BufferedInputFile(image_file.read(), filename="pic_keys.jpg"), + photo=BufferedInputFile( + image_file.read(), filename="pic_keys.jpg" + ), caption=response_message, reply_markup=keyboard, ) @@ -121,7 +153,9 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session time_left = expiry_date - current_date if time_left.total_seconds() <= 0: - days_left_message = "🕒 Статус подписки:\n🔴 Истекла\nОсталось часов: 0" + days_left_message = ( + "🕒 Статус подписки:\n🔴 Истекла\nОсталось часов: 0" + ) elif time_left.days > 0: days_left_message = f"Осталось дней: {time_left.days}" else: @@ -129,33 +163,54 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session 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) + response_message = key_message( + key, formatted_expiry_date, days_left_message, server_name + ) builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton(text="🍏 Скачать для iOS", url=DOWNLOAD_IOS), - InlineKeyboardButton(text="🤖 Скачать для Android", url=DOWNLOAD_ANDROID), + InlineKeyboardButton( + text="🤖 Скачать для Android", url=DOWNLOAD_ANDROID + ), ) builder.row( - InlineKeyboardButton(text="🍏 Подключить на iOS", url=f"{CONNECT_IOS}{key}"), - InlineKeyboardButton(text="🤖 Подключить на Android", url=f"{CONNECT_ANDROID}{key}"), + InlineKeyboardButton( + text="🍏 Подключить на iOS", url=f"{CONNECT_IOS}{key}" + ), + InlineKeyboardButton( + text="🤖 Подключить на Android", url=f"{CONNECT_ANDROID}{key}" + ), ) - builder.row(InlineKeyboardButton(text="💻 Windows/Linux", callback_data=f"connect_pc|{key_name}")) + builder.row( + InlineKeyboardButton( + text="💻 Windows/Linux", callback_data=f"connect_pc|{key_name}" + ) + ) builder.row( - InlineKeyboardButton(text="⏳ Продлить", callback_data=f"renew_key|{key_name}"), - InlineKeyboardButton(text="❌ Удалить", callback_data=f"delete_key|{key_name}"), + InlineKeyboardButton( + text="⏳ Продлить", callback_data=f"renew_key|{key_name}" + ), + InlineKeyboardButton( + text="❌ Удалить", callback_data=f"delete_key|{key_name}" + ), ) if not key.startswith(PUBLIC_LINK): builder.row( - InlineKeyboardButton(text="🔄 Обновить подписку", callback_data=f"update_subscription|{key_name}") + InlineKeyboardButton( + text="🔄 Обновить подписку", + callback_data=f"update_subscription|{key_name}", + ) ) - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + ) keyboard = builder.as_markup() @@ -184,7 +239,9 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session @router.callback_query(F.data.startswith("update_subscription|")) -async def process_callback_update_subscription(callback_query: types.CallbackQuery, session: Any): +async def process_callback_update_subscription( + callback_query: types.CallbackQuery, session: Any +): tg_id = callback_query.message.chat.id email = callback_query.data.split("|")[1] try: @@ -244,7 +301,9 @@ async def process_callback_update_subscription(callback_query: types.CallbackQue else: await callback_query.message.answer("Ключ не найден в базе данных.") except Exception as e: - await handle_error(tg_id, callback_query, f"Ошибка при обновлении подписки: {e}") + await handle_error( + tg_id, callback_query, f"Ошибка при обновлении подписки: {e}" + ) @router.callback_query(F.data.startswith("delete_key|")) @@ -259,7 +318,11 @@ async def process_callback_delete_key(callback_query: types.CallbackQuery): callback_data=f"confirm_delete|{client_id}", ) ], - [types.InlineKeyboardButton(text="❌ Нет, отменить", callback_data="view_keys")], + [ + types.InlineKeyboardButton( + text="❌ Нет, отменить", callback_data="view_keys" + ) + ], ] ) @@ -319,14 +382,18 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio callback_data=f"renew_plan|12|{client_id}", ) ) - back_button = InlineKeyboardButton(text="🔙 Назад", callback_data="view_keys") + back_button = InlineKeyboardButton( + text="🔙 Назад", callback_data="view_keys" + ) builder.row(back_button) 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"), + expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime( + "%Y-%m-%d %H:%M:%S" + ), ) await callback_query.message.answer( @@ -340,15 +407,21 @@ async def process_callback_renew_key(callback_query: types.CallbackQuery, sessio @router.callback_query(F.data.startswith("confirm_delete|")) -async def process_callback_confirm_delete(callback_query: types.CallbackQuery, session: Any): +async def process_callback_confirm_delete( + callback_query: types.CallbackQuery, session: Any +): email = callback_query.data.split("|")[1] try: - record = await session.fetchrow("SELECT client_id FROM keys WHERE email = $1", email) + record = await session.fetchrow( + "SELECT client_id FROM keys WHERE email = $1", email + ) if record: client_id = record["client_id"] 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) @@ -363,7 +436,9 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery, s try: tasks = [] for cluster_id, cluster in servers.items(): - tasks.append(delete_key_from_cluster(cluster_id, email, client_id)) + tasks.append( + delete_key_from_cluster(cluster_id, email, client_id) + ) await asyncio.gather(*tasks) @@ -376,7 +451,9 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery, s 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 callback_query.message.answer( @@ -388,7 +465,9 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery, s @router.callback_query(F.data.startswith("renew_plan|")) -async def process_callback_renew_plan(callback_query: types.CallbackQuery, session: Any): +async def process_callback_renew_plan( + callback_query: types.CallbackQuery, session: Any +): tg_id = callback_query.message.chat.id plan, client_id = ( callback_query.data.split("|")[1], @@ -411,17 +490,27 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery, sessi 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"] balance = await get_balance(tg_id) if balance < cost: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="Пополнить баланс", callback_data="pay")) - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton(text="Пополнить баланс", callback_data="pay") + ) + builder.row( + InlineKeyboardButton( + text="👤 Личный кабинет", callback_data="profile" + ) + ) await callback_query.message.answer( INSUFFICIENT_FUNDS_MSG, @@ -429,11 +518,17 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery, sessi ) return - response_message = SUCCESS_RENEWAL_MSG.format(months=RENEWAL_PLANS[plan]["months"]) + response_message = SUCCESS_RENEWAL_MSG.format( + months=RENEWAL_PLANS[plan]["months"] + ) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + ) - await callback_query.message.answer(response_message, reply_markup=builder.as_markup()) + await callback_query.message.answer( + response_message, reply_markup=builder.as_markup() + ) servers = await get_servers_from_db() diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py index 45b0d3fc..0868d84f 100644 --- a/handlers/keys/subscriptions.py +++ b/handlers/keys/subscriptions.py @@ -2,10 +2,10 @@ import base64 from datetime import datetime import aiohttp -from aiohttp import web import asyncpg - +from aiohttp import web from config import DATABASE_URL, TRANSITION_DATE_STR + from database import get_servers_from_db from logger import logger @@ -20,7 +20,9 @@ async def fetch_url_content(url, tg_id): logger.info(f"Успешно получен контент с {url} для tg_id: {tg_id}") return base64.b64decode(content).decode("utf-8").split("\n") else: - logger.error(f"Не удалось получить {url} для tg_id: {tg_id}, статус: {response.status}") + logger.error( + f"Не удалось получить {url} для tg_id: {tg_id}, статус: {response.status}" + ) return [] except Exception as e: logger.error(f"Ошибка при получении {url} для tg_id: {tg_id}: {e}") @@ -29,7 +31,9 @@ async def fetch_url_content(url, tg_id): async def combine_unique_lines(urls, tg_id, query_string): all_lines = [] - logger.info(f"Начинаем объединение подписок для tg_id: {tg_id}, запрос: {query_string}") + logger.info( + f"Начинаем объединение подписок для tg_id: {tg_id}, запрос: {query_string}" + ) urls_with_query = [f"{url}?{query_string}" for url in urls] logger.info(f"Составлены URL-адреса: {urls_with_query}") @@ -39,7 +43,9 @@ async def combine_unique_lines(urls, tg_id, query_string): all_lines.extend(lines) all_lines = list(set(filter(None, all_lines))) - logger.info(f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов для tg_id: {tg_id}") + logger.info( + f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов для tg_id: {tg_id}" + ) return all_lines @@ -50,7 +56,9 @@ transition_timestamp_ms = int(transition_date.timestamp() * 1000) transition_timestamp_ms_adjusted = transition_timestamp_ms - (3 * 60 * 60 * 1000) -logger.info(f"Время перехода (с поправкой на часовой пояс): {transition_timestamp_ms_adjusted}") +logger.info( + f"Время перехода (с поправкой на часовой пояс): {transition_timestamp_ms_adjusted}" +) async def handle_old_subscription(request): @@ -67,7 +75,9 @@ async def handle_old_subscription(request): conn = await asyncpg.connect(DATABASE_URL) try: - key_info = await conn.fetchrow("SELECT created_at FROM keys WHERE email = $1", email) + key_info = await conn.fetchrow( + "SELECT created_at FROM keys WHERE email = $1", email + ) if not key_info: logger.warning(f"Клиент с email {email} не найден в базе.") @@ -80,7 +90,9 @@ async def handle_old_subscription(request): logger.info(f"Значение created_at для клиента с email {email}: {created_at_ms}") created_at_datetime = datetime.utcfromtimestamp(created_at_ms / 1000) - logger.info(f"Время создания клиента в формате datetime (UTC): {created_at_datetime}") + logger.info( + f"Время создания клиента в формате datetime (UTC): {created_at_datetime}" + ) if created_at_ms >= transition_timestamp_ms_adjusted: logger.info(f"Клиент с email {email} является новым.") @@ -99,7 +111,9 @@ async def handle_old_subscription(request): combined_subscriptions = await combine_unique_lines(urls, email, "") - 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", @@ -130,7 +144,9 @@ async def handle_new_subscription(request): conn = await asyncpg.connect(DATABASE_URL) try: - client_data = await conn.fetchrow("SELECT tg_id FROM keys WHERE email = $1", email) + client_data = await conn.fetchrow( + "SELECT tg_id FROM keys WHERE email = $1", email + ) if not client_data: logger.warning(f"Клиент с email {email} не найден в базе.") @@ -163,7 +179,9 @@ async def handle_new_subscription(request): combined_subscriptions = await combine_unique_lines(urls, tg_id, 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", diff --git a/handlers/keys/trial_key.py b/handlers/keys/trial_key.py index 819d66da..fa0f9745 100644 --- a/handlers/keys/trial_key.py +++ b/handlers/keys/trial_key.py @@ -1,12 +1,12 @@ import asyncio +import uuid from datetime import datetime, timedelta from typing import Any -import uuid +from config import ADMIN_PASSWORD, ADMIN_USERNAME, PUBLIC_LINK, TOTAL_GB, TRIAL_TIME from py3xui import AsyncApi from client import add_client -from config import ADMIN_PASSWORD, ADMIN_USERNAME, PUBLIC_LINK, TOTAL_GB, TRIAL_TIME from database import get_servers_from_db, store_key, use_trial from handlers.texts import INSTRUCTIONS from handlers.utils import generate_random_email, get_least_loaded_cluster diff --git a/handlers/notifications.py b/handlers/notifications.py index 7ac8ccf2..c16832f8 100644 --- a/handlers/notifications.py +++ b/handlers/notifications.py @@ -1,12 +1,20 @@ import asyncio from datetime import datetime, timedelta +import asyncpg from aiogram import Bot, Router, types from aiogram.utils.keyboard import InlineKeyboardBuilder -import asyncpg +from config import ( + ADMIN_PASSWORD, + ADMIN_USERNAME, + DATABASE_URL, + DEV_MODE, + RENEWAL_PLANS, + TOTAL_GB, + TRIAL_TIME, +) from py3xui import AsyncApi -from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, DEV_MODE, RENEWAL_PLANS, TOTAL_GB, TRIAL_TIME from database import ( add_notification, check_notification_time, @@ -30,8 +38,12 @@ async def notify_expiring_keys(bot: Bot): logger.info("Подключение к базе данных успешно.") current_time = int(datetime.utcnow().timestamp() * 1000) - threshold_time_10h = int((datetime.utcnow() + timedelta(hours=10)).timestamp() * 1000) - threshold_time_24h = int((datetime.utcnow() + timedelta(days=1)).timestamp() * 1000) + threshold_time_10h = int( + (datetime.utcnow() + timedelta(hours=10)).timestamp() * 1000 + ) + threshold_time_24h = int( + (datetime.utcnow() + timedelta(days=1)).timestamp() * 1000 + ) logger.info("Начало обработки уведомлений.") @@ -60,10 +72,14 @@ async def is_bot_blocked(bot: Bot, chat_id: int) -> bool: try: member = await bot.get_chat_member(chat_id, bot.id) blocked = member.status == "left" - logger.info(f"Статус бота для пользователя {chat_id}: {'заблокирован' if blocked else 'активен'}") + logger.info( + f"Статус бота для пользователя {chat_id}: {'заблокирован' if blocked else 'активен'}" + ) return blocked except Exception as e: - logger.warning(f"Не удалось проверить статус бота для пользователя {chat_id}: {e}") + logger.warning( + f"Не удалось проверить статус бота для пользователя {chat_id}: {e}" + ) return False @@ -110,7 +126,9 @@ async def notify_10h_keys( if not await is_bot_blocked(bot, tg_id) and not DEV_MODE: try: keyboard = InlineKeyboardBuilder() - keyboard.button(text="🔄 Продлить VPN", callback_data=f'renew_key|{email}') + keyboard.button( + text="🔄 Продлить VPN", callback_data=f"renew_key|{email}" + ) keyboard.button(text="💳 Пополнить баланс", callback_data="pay") keyboard.button(text="👤 Личный кабинет", callback_data="profile") keyboard.adjust(1) @@ -118,7 +136,9 @@ async def notify_10h_keys( 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}") + logger.error( + f"Ошибка при отправке уведомления пользователю {tg_id}: {e}" + ) continue await conn.execute( @@ -177,7 +197,7 @@ async def notify_24h_keys( builder.row( types.InlineKeyboardButton( text="🔄 Продлить VPN", - callback_data=f'renew_key|{email}', + callback_data=f"renew_key|{email}", ) ) builder.row( @@ -196,14 +216,18 @@ async def notify_24h_keys( 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}") + 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']}.") + logger.info( + f"Обновлено поле notified_24h для клиента {record['client_id']}." + ) await asyncio.sleep(1) @@ -225,18 +249,27 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): logger.info(f"Найдено {len(inactive_trial_users)} неактивных пользователей.") for user in inactive_trial_users: - tg_id = user['tg_id'] - username = user.get('username', 'Пользователь') + tg_id = user["tg_id"] + username = user.get("username", "Пользователь") try: - can_notify = await check_notification_time(tg_id, 'inactive_trial', hours=24, session=conn) + can_notify = await check_notification_time( + tg_id, "inactive_trial", hours=24, session=conn + ) if can_notify and not await is_bot_blocked(bot, tg_id): builder = InlineKeyboardBuilder() builder.row( - types.InlineKeyboardButton(text="🚀 Активировать пробный период", callback_data="create_key") + types.InlineKeyboardButton( + text="🚀 Активировать пробный период", + callback_data="create_key", + ) + ) + builder.row( + types.InlineKeyboardButton( + text="👤 Личный кабинет", callback_data="profile" + ) ) - builder.row(types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) keyboard = builder.as_markup() message = ( @@ -249,10 +282,12 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection): await bot.send_message(tg_id, message, reply_markup=keyboard) logger.info(f"Отправлено уведомление неактивному пользователю {tg_id}.") - await add_notification(tg_id, 'inactive_trial', session=conn) + await add_notification(tg_id, "inactive_trial", session=conn) except Exception as e: - logger.error(f"Ошибка при отправке уведомления неактивному пользователю {tg_id}: {e}") + logger.error( + f"Ошибка при отправке уведомления неактивному пользователю {tg_id}: {e}" + ) await asyncio.sleep(1) @@ -286,20 +321,32 @@ async def process_key(record, bot, conn): f"Время истечения ключа: {expiry_time} (дата: {expiry_date}), Текущее время: {current_date}, Оставшееся время: {time_left}" ) keyboard = types.InlineKeyboardMarkup( - inline_keyboard=[[types.InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")]] + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text="👤 Личный кабинет", callback_data="profile" + ) + ] + ] ) try: if balance >= RENEWAL_PLANS["1"]["price"]: await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"]) - 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) servers = await get_servers_from_db() for cluster_id in servers: - await renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, TOTAL_GB) - logger.info(f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}.") + await renew_key_in_cluster( + cluster_id, email, client_id, new_expiry_time, TOTAL_GB + ) + logger.info( + f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}." + ) await conn.execute( """ @@ -309,10 +356,14 @@ async def process_key(record, bot, conn): """, client_id, ) - logger.info(f"Флаги notified и notified_24 сброшены для клиента с ID {client_id}.") + logger.info( + f"Флаги notified и notified_24 сброшены для клиента с ID {client_id}." + ) try: await bot.send_message(tg_id, text=KEY_RENEWED, reply_markup=keyboard) - logger.info(f"Уведомление об успешном продлении отправлено клиенту {tg_id}.") + logger.info( + f"Уведомление об успешном продлении отправлено клиенту {tg_id}." + ) except Exception as e: logger.error(f"Ошибка при отправке уведомления клиенту {tg_id}: {e}") @@ -320,10 +371,16 @@ async def process_key(record, bot, conn): message_expired = "Ваша подписка истекла и была удалена. Получите новую через личный кабинет" try: - await bot.send_message(tg_id, text=message_expired, reply_markup=keyboard) - logger.info(f"Уведомление об истечении подписки и удалении ключа отправлено пользователю {tg_id}.") + await bot.send_message( + tg_id, text=message_expired, reply_markup=keyboard + ) + logger.info( + f"Уведомление об истечении подписки и удалении ключа отправлено пользователю {tg_id}." + ) except Exception as e: - logger.error(f"Ошибка при отправке уведомления об истечении подписки пользователю {tg_id}: {e}") + logger.error( + f"Ошибка при отправке уведомления об истечении подписки пользователю {tg_id}: {e}" + ) servers = await get_servers_from_db() @@ -343,7 +400,9 @@ async def check_online_users(): for cluster_id, cluster in servers.items(): for server_id, server in enumerate(cluster): - xui = AsyncApi(server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD) + xui = AsyncApi( + server["api_url"], username=ADMIN_USERNAME, password=ADMIN_PASSWORD + ) await xui.login() try: online_users = len(await xui.client.online()) @@ -351,4 +410,6 @@ async def check_online_users(): f"Сервер '{server['server_name']}' доступен, текущее количество активных пользователей: {online_users}." ) except Exception as e: - logger.error(f"Не удалось проверить пользователей на сервере {server_id}: {e}") + logger.error( + f"Не удалось проверить пользователей на сервере {server_id}: {e}" + ) diff --git a/handlers/pay.py b/handlers/pay.py index bd709cd0..a2ef2e01 100644 --- a/handlers/pay.py +++ b/handlers/pay.py @@ -1,8 +1,13 @@ from aiogram import F, Router from aiogram.types import CallbackQuery, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder - -from config import CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, ROBOKASSA_ENABLE, STARS_ENABLE, YOOKASSA_ENABLE +from config import ( + CRYPTO_BOT_ENABLE, + FREEKASSA_ENABLE, + ROBOKASSA_ENABLE, + STARS_ENABLE, + YOOKASSA_ENABLE, +) router = Router() @@ -46,8 +51,14 @@ async def handle_pay(callback_query: CallbackQuery): callback_data="pay_robokassa", ) ) - builder.row(InlineKeyboardButton(text="🎟️ Активировать купон", callback_data="activate_coupon")) - builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")) + builder.row( + InlineKeyboardButton( + text="🎟️ Активировать купон", callback_data="activate_coupon" + ) + ) + builder.row( + InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate") + ) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) await callback_query.message.answer( diff --git a/handlers/payments/__init__.py b/handlers/payments/__init__.py index 231dd68a..2b7063b5 100644 --- a/handlers/payments/__init__.py +++ b/handlers/payments/__init__.py @@ -1,15 +1,21 @@ -__all__ = ('router',) +__all__ = ("router",) from aiogram import Router +from config import ( + CRYPTO_BOT_ENABLE, + FREEKASSA_ENABLE, + ROBOKASSA_ENABLE, + STARS_ENABLE, + YOOKASSA_ENABLE, +) -from config import ROBOKASSA_ENABLE, STARS_ENABLE, CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, YOOKASSA_ENABLE from .cryprobot_pay import router as cryprobot_router from .freekassa_pay import router as freekassa_router from .robokassa_pay import router as robokassa_router from .stars_pay import router as stars_router from .yookassa_pay import router as yookassa_router -router = Router(name='payments_main_router') +router = Router(name="payments_main_router") if YOOKASSA_ENABLE: router.include_router(yookassa_router) diff --git a/handlers/payments/cryprobot_pay.py b/handlers/payments/cryprobot_pay.py index 6265d4db..a70d8758 100644 --- a/handlers/payments/cryprobot_pay.py +++ b/handlers/payments/cryprobot_pay.py @@ -7,9 +7,15 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web - from config import CRYPTO_BOT_ENABLE, CRYPTO_BOT_TOKEN, RUB_TO_USDT -from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance + +from database import ( + add_connection, + add_payment, + check_connection_exists, + get_key_count, + update_balance, +) from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger @@ -27,7 +33,9 @@ class ReplenishBalanceState(StatesGroup): @router.callback_query(F.data == "pay_cryptobot") -async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, state: FSMContext, session: Any): +async def process_callback_pay_cryptobot( + callback_query: types.CallbackQuery, state: FSMContext, session: Any +): builder = InlineKeyboardBuilder() for i in range(0, len(PAYMENT_OPTIONS), 2): if i + 1 < len(PAYMENT_OPTIONS): @@ -59,7 +67,12 @@ async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, st if key_count == 0: exists = await check_connection_exists(callback_query.message.chat.id) if not exists: - await add_connection(tg_id=callback_query.message.chat.id, balance=0.0, trial=0, session=session) + await add_connection( + tg_id=callback_query.message.chat.id, + balance=0.0, + trial=0, + session=session, + ) await callback_query.message.answer( "Выберите сумму пополнения:", reply_markup=builder.as_markup(), @@ -68,7 +81,9 @@ async def process_callback_pay_cryptobot(callback_query: types.CallbackQuery, st @router.callback_query(F.data.startswith("crypto_amount|")) -async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): +async def process_amount_selection( + callback_query: types.CallbackQuery, state: FSMContext +): data = callback_query.data.split("|", 1) if len(data) != 2: @@ -95,7 +110,9 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F if hasattr(invoice, "bot_invoice_url"): builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="Пополнить", url=invoice.bot_invoice_url)) + builder.row( + InlineKeyboardButton(text="Пополнить", url=invoice.bot_invoice_url) + ) builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay")) await callback_query.message.answer( text=f"Вы выбрали пополнение на {amount} рублей.", @@ -115,7 +132,9 @@ async def cryptobot_webhook(request): await process_crypto_payment(data["payload"]) return web.Response(status=200) else: - logger.warning(f"Неподдерживаемый тип обновления: {data.get('update_type')}") + logger.warning( + f"Неподдерживаемый тип обновления: {data.get('update_type')}" + ) return web.Response(status=400) except Exception as e: logger.error(f"Ошибка обработки вебхука: {e}") @@ -140,8 +159,9 @@ async def process_crypto_payment(payload): @router.callback_query(F.data == "enter_custom_amount_crypto") -async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): - +async def process_enter_custom_amount( + callback_query: types.CallbackQuery, state: FSMContext +): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="pay_cryptobot")) @@ -158,11 +178,15 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) if message.text.isdigit(): amount = int(message.text) if amount // RUB_TO_USDT <= 0: - await message.answer(f"Сумма должна быть больше {RUB_TO_USDT}. Пожалуйста, введите сумму еще раз:") + await message.answer( + f"Сумма должна быть больше {RUB_TO_USDT}. Пожалуйста, введите сумму еще раз:" + ) return await state.update_data(amount=amount) - await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_crypto) + await state.set_state( + ReplenishBalanceState.waiting_for_payment_confirmation_crypto + ) try: invoice = await crypto.create_invoice( asset="USDT", @@ -173,7 +197,9 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) if hasattr(invoice, "bot_invoice_url"): builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="Пополнить", url=invoice.bot_invoice_url)) + builder.row( + InlineKeyboardButton(text="Пополнить", url=invoice.bot_invoice_url) + ) builder.row( InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"), ) diff --git a/handlers/payments/freekassa_pay.py b/handlers/payments/freekassa_pay.py index 7c423129..52b50116 100644 --- a/handlers/payments/freekassa_pay.py +++ b/handlers/payments/freekassa_pay.py @@ -4,15 +4,15 @@ import logging import time import uuid +import requests from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web -import requests - from config import FREEKASSA_API_KEY, FREEKASSA_SHOP_ID + from database import add_payment, update_balance from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS @@ -49,7 +49,9 @@ async def create_payment(user_id, amount, email, ip): 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}") @@ -83,7 +85,9 @@ async def freekassa_webhook(request): @router.callback_query(lambda c: c.data == "pay_freekassa") -async def process_callback_pay_freekassa(callback_query: types.CallbackQuery, state: FSMContext): +async def process_callback_pay_freekassa( + callback_query: types.CallbackQuery, state: FSMContext +): builder = InlineKeyboardBuilder() for i in range(0, len(PAYMENT_OPTIONS), 2): if i + 1 < len(PAYMENT_OPTIONS): @@ -121,7 +125,9 @@ async def process_callback_pay_freekassa(callback_query: types.CallbackQuery, st @router.callback_query(F.data.startswith("freekassa_amount|")) -async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): +async def process_amount_selection( + callback_query: types.CallbackQuery, state: FSMContext +): data = callback_query.data.split("|", 1) amount_str = data[1] try: @@ -132,12 +138,18 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F user_email = f"{callback_query.message.chat.id}@solo.net" user_ip = callback_query.message.chat.id - payment_url = await create_payment(callback_query.message.chat.id, amount, user_email, user_ip) + payment_url = await create_payment( + callback_query.message.chat.id, amount, user_email, user_ip + ) if payment_url: confirm_keyboard = InlineKeyboardMarkup( inline_keyboard=[ - [InlineKeyboardButton(text=f"Оплатить {amount} рублей", url=payment_url)], + [ + InlineKeyboardButton( + text=f"Оплатить {amount} рублей", url=payment_url + ) + ], [InlineKeyboardButton(text="⬅️ Назад", callback_data="pay")], ] ) @@ -153,7 +165,9 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F @router.callback_query(F.data == "enter_custom_amount_freekassa") -async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): +async def process_enter_custom_amount( + callback_query: types.CallbackQuery, state: FSMContext +): await callback_query.message.answer(text="Введите сумму пополнения:") await state.set_state(ReplenishBalanceState.entering_custom_amount_freekassa) @@ -163,7 +177,9 @@ 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.chat.id}@solo.net" @@ -171,7 +187,9 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) payment_url = await create_payment(message.chat.id, amount, user_email, user_ip) if payment_url: - keyboard = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton("Оплатить", url=payment_url)]]) + keyboard = InlineKeyboardMarkup( + inline_keyboard=[[InlineKeyboardButton("Оплатить", url=payment_url)]] + ) await message.answer( f"Вы выбрали оплату на {amount} рублей. Перейдите по ссылке для завершения оплаты:", reply_markup=keyboard, diff --git a/handlers/payments/robokassa_pay.py b/handlers/payments/robokassa_pay.py index 9ae90a29..2c1de2e5 100644 --- a/handlers/payments/robokassa_pay.py +++ b/handlers/payments/robokassa_pay.py @@ -7,10 +7,22 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web +from config import ( + ROBOKASSA_ENABLE, + ROBOKASSA_LOGIN, + ROBOKASSA_PASSWORD1, + ROBOKASSA_PASSWORD2, + ROBOKASSA_TEST_MODE, +) from robokassa import HashAlgorithm, Robokassa -from config import ROBOKASSA_ENABLE, ROBOKASSA_LOGIN, ROBOKASSA_PASSWORD1, ROBOKASSA_PASSWORD2, ROBOKASSA_TEST_MODE -from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance +from database import ( + add_connection, + add_payment, + check_connection_exists, + get_key_count, + update_balance, +) from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger @@ -37,7 +49,9 @@ if ROBOKASSA_ENABLE: def generate_payment_link(amount, inv_id, description, tg_id): """Генерация ссылки на оплату.""" - logger.debug(f"Generating payment link for amount: {amount}, inv_id: {inv_id}, description: {description}") + logger.debug( + f"Generating payment link for amount: {amount}, inv_id: {inv_id}, description: {description}" + ) payment_link = robokassa._payment.link.generate_by_script( out_sum=amount, inv_id=inv_id, @@ -49,7 +63,9 @@ def generate_payment_link(amount, inv_id, description, tg_id): @router.callback_query(F.data == "pay_robokassa") -async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, state: FSMContext, session: Any): +async def process_callback_pay_robokassa( + callback_query: types.CallbackQuery, state: FSMContext, session: Any +): tg_id = callback_query.message.chat.id logger.info(f"User {tg_id} initiated Robokassa payment.") @@ -98,7 +114,9 @@ async def process_callback_pay_robokassa(callback_query: types.CallbackQuery, st @router.callback_query(F.data.startswith("robokassa_amount|")) -async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): +async def process_amount_selection( + callback_query: types.CallbackQuery, state: FSMContext +): logger.info(f"Получены данные callback_data: {callback_query.data}") data = callback_query.data.split("|") @@ -152,7 +170,9 @@ async def robokassa_webhook(request): shp_id = params.get("shp_id") signature_value = params.get("SignatureValue") - logger.info(f"OutSum: {amount}, InvId: {inv_id}, shp_id: {shp_id}, SignatureValue: {signature_value}") + logger.info( + f"OutSum: {amount}, InvId: {inv_id}, shp_id: {shp_id}, SignatureValue: {signature_value}" + ) if not check_payment_signature(params): logger.error("Неверная подпись или данные запроса.") @@ -191,7 +211,9 @@ def check_payment_signature(params): logger.info(f"Signature string before hashing: {signature_string}") - expected_signature = hashlib.md5(signature_string.encode("utf-8")).hexdigest().upper() + expected_signature = ( + hashlib.md5(signature_string.encode("utf-8")).hexdigest().upper() + ) logger.info(f"Expected signature: {expected_signature}") logger.info(f"Received signature: {signature_value}") @@ -200,7 +222,9 @@ def check_payment_signature(params): @router.callback_query(F.data == "enter_custom_amount_robokassa") -async def process_custom_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): +async def process_custom_amount_selection( + callback_query: types.CallbackQuery, state: FSMContext +): tg_id = callback_query.message.chat.id logger.info(f"User {tg_id} chose to enter a custom amount.") @@ -212,7 +236,9 @@ async def process_custom_amount_selection(callback_query: types.CallbackQuery, s reply_markup=builder.as_markup(), ) - await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa) + await state.set_state( + ReplenishBalanceState.waiting_for_payment_confirmation_robokassa + ) @router.message(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa) @@ -246,4 +272,6 @@ async def handle_custom_amount_input(message: types.Message, state: FSMContext): await state.clear() except ValueError as e: logger.error(f"Некорректная сумма от пользователя {tg_id}: {e}") - await message.answer(text="Введите корректную сумму в рублях (целое положительное число).") + await message.answer( + text="Введите корректную сумму в рублях (целое положительное число)." + ) diff --git a/handlers/payments/stars_pay.py b/handlers/payments/stars_pay.py index 698444b8..f993d3f9 100644 --- a/handlers/payments/stars_pay.py +++ b/handlers/payments/stars_pay.py @@ -5,9 +5,15 @@ from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, LabeledPrice, PreCheckoutQuery from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import RUB_TO_XTR -from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance + +from database import ( + add_connection, + add_payment, + check_connection_exists, + get_key_count, + update_balance, +) from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger @@ -22,11 +28,17 @@ class ReplenishBalanceState(StatesGroup): @router.callback_query(F.data == "pay_stars") -async def process_callback_pay_stars(callback_query: types.CallbackQuery, state: FSMContext, session: Any): +async def process_callback_pay_stars( + callback_query: types.CallbackQuery, state: FSMContext, session: Any +): tg_id = callback_query.message.chat.id builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot")) + builder.row( + InlineKeyboardButton( + text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot" + ) + ) for i in range(0, len(PAYMENT_OPTIONS), 2): if i + 1 < len(PAYMENT_OPTIONS): @@ -76,7 +88,9 @@ async def process_callback_pay_stars(callback_query: types.CallbackQuery, state: @router.callback_query(F.data.startswith("stars_amount|")) -async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): +async def process_amount_selection( + callback_query: types.CallbackQuery, state: FSMContext +): data = callback_query.data.split("|", 1) if len(data) != 2: @@ -104,7 +118,6 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_stars) try: - builder = InlineKeyboardBuilder() builder.row( InlineKeyboardButton(text="Пополнить", pay=True), @@ -128,8 +141,9 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F @router.callback_query(F.data == "enter_custom_amount_stars") -async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): - +async def process_enter_custom_amount( + callback_query: types.CallbackQuery, state: FSMContext +): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="pay_stars")) @@ -146,11 +160,15 @@ async def process_custom_amount_input(message: types.Message, state: FSMContext) if message.text.isdigit(): amount = int(message.text) if amount // RUB_TO_XTR <= 0: - await message.answer(f"Сумма должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:") + await message.answer( + f"Сумма должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:" + ) return await state.update_data(amount=amount) - await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_stars) + await state.set_state( + ReplenishBalanceState.waiting_for_payment_confirmation_stars + ) try: builder = InlineKeyboardBuilder() builder.row( diff --git a/handlers/payments/utils.py b/handlers/payments/utils.py index 21015090..430c12f0 100644 --- a/handlers/payments/utils.py +++ b/handlers/payments/utils.py @@ -8,7 +8,9 @@ from logger import logger async def send_payment_success_notification(user_id: int, amount: float): try: builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + ) await bot.send_message( chat_id=user_id, text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!", diff --git a/handlers/payments/yookassa_pay.py b/handlers/payments/yookassa_pay.py index c1687107..62760122 100644 --- a/handlers/payments/yookassa_pay.py +++ b/handlers/payments/yookassa_pay.py @@ -1,5 +1,5 @@ -from typing import Any import uuid +from typing import Any from aiogram import F, Router, types from aiogram.fsm.context import FSMContext @@ -7,10 +7,16 @@ from aiogram.fsm.state import State, StatesGroup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.utils.keyboard import InlineKeyboardBuilder from aiohttp import web +from config import YOOKASSA_ENABLE, YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID from yookassa import Configuration, Payment -from config import YOOKASSA_ENABLE, YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID -from database import add_connection, add_payment, check_connection_exists, get_key_count, update_balance +from database import ( + add_connection, + add_payment, + check_connection_exists, + get_key_count, + update_balance, +) from handlers.payments.utils import send_payment_success_notification from handlers.texts import PAYMENT_OPTIONS from logger import logger @@ -31,7 +37,9 @@ class ReplenishBalanceState(StatesGroup): @router.callback_query(F.data == "pay_yookassa") -async def process_callback_pay_yookassa(callback_query: types.CallbackQuery, state: FSMContext, session: Any): +async def process_callback_pay_yookassa( + callback_query: types.CallbackQuery, state: FSMContext, session: Any +): tg_id = callback_query.message.chat.id builder = InlineKeyboardBuilder() @@ -79,7 +87,9 @@ async def process_callback_pay_yookassa(callback_query: types.CallbackQuery, sta @router.callback_query(F.data.startswith("yookassa_amount|")) -async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext): +async def process_amount_selection( + callback_query: types.CallbackQuery, state: FSMContext +): data = callback_query.data.split("|", 1) if len(data) != 2: @@ -92,7 +102,9 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F return await state.update_data(amount=amount) - await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_yookassa) + await state.set_state( + ReplenishBalanceState.waiting_for_payment_confirmation_yookassa + ) # state_data = await state.get_data() customer_name = callback_query.from_user.full_name @@ -167,8 +179,9 @@ async def yookassa_webhook(request): @router.callback_query(F.data == "enter_custom_amount_yookassa") -async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext): - +async def process_enter_custom_amount( + callback_query: types.CallbackQuery, state: FSMContext +): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="pay_yookassa")) @@ -185,11 +198,15 @@ 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 await state.update_data(amount=amount) - await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_yookassa) + await state.set_state( + ReplenishBalanceState.waiting_for_payment_confirmation_yookassa + ) try: payment = Payment.create( diff --git a/handlers/profile.py b/handlers/profile.py index 80775e0e..49477276 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -4,8 +4,8 @@ from aiogram import F, Router, types from aiogram.fsm.context import FSMContext from aiogram.types import BufferedInputFile, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder - from config import NEWS_MESSAGE, RENEWAL_PLANS + from database import get_balance, get_key_count, get_referral_stats from handlers.texts import get_referral_link, invite_message_send, profile_message_send @@ -13,7 +13,9 @@ router = Router() @router.callback_query(F.data == "profile") -async def process_callback_view_profile(callback_query: types.CallbackQuery, state: FSMContext, admin: bool): +async def process_callback_view_profile( + callback_query: types.CallbackQuery, state: FSMContext, admin: bool +): chat_id = callback_query.message.chat.id username = callback_query.from_user.full_name image_path = os.path.join("img", "pic.jpg") @@ -46,7 +48,9 @@ async def process_callback_view_profile(callback_query: types.CallbackQuery, sta ) builder.row(InlineKeyboardButton(text="💡 Тарифы", callback_data="view_tariffs")) if admin: - builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")) + builder.row( + InlineKeyboardButton(text="🔧 Администратор", callback_data="admin") + ) builder.row(InlineKeyboardButton(text="⬅️ Главное меню", callback_data="start")) if os.path.isfile(image_path): @@ -68,18 +72,15 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery): builder = InlineKeyboardBuilder() builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) - image_path = os.path.join("img", "tariffs.jpg") + image_path = os.path.join("img", "tariffs.jpg") - tariffs_message = ( - "🚀 Доступные тарифы VPN:\n\n" - + "\n".join( - [ - f"{months} {'месяц' if months == '1' else 'месяца' if int(months) in [2, 3, 4] else 'месяцев'}: " - f"{RENEWAL_PLANS[months]['price']} " - f"{'💳' if months == '1' else '🌟' if months == '3' else '🔥' if months == '6' else '🚀'} рублей" - for months in sorted(RENEWAL_PLANS.keys(), key=int) - ] - ) + tariffs_message = "🚀 Доступные тарифы VPN:\n\n" + "\n".join( + [ + f"{months} {'месяц' if months == '1' else 'месяца' if int(months) in [2, 3, 4] else 'месяцев'}: " + f"{RENEWAL_PLANS[months]['price']} " + f"{'💳' if months == '1' else '🌟' if months == '3' else '🔥' if months == '6' else '🚀'} рублей" + for months in sorted(RENEWAL_PLANS.keys(), key=int) + ] ) if os.path.isfile(image_path): @@ -96,7 +97,6 @@ async def view_tariffs_handler(callback_query: types.CallbackQuery): ) - @router.callback_query(F.data == "invite") async def invite_handler(callback_query: types.CallbackQuery): chat_id = callback_query.message.chat.id diff --git a/handlers/start.py b/handlers/start.py index 93df5511..04d0847d 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -4,11 +4,29 @@ from typing import Any from aiogram import F, Router from aiogram.filters import Command from aiogram.fsm.context import FSMContext -from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton, Message +from aiogram.types import ( + BufferedInputFile, + CallbackQuery, + InlineKeyboardButton, + Message, +) from aiogram.utils.keyboard import InlineKeyboardBuilder +from config import ( + CHANNEL_URL, + CONNECT_ANDROID, + CONNECT_IOS, + DOWNLOAD_ANDROID, + DOWNLOAD_IOS, + SUPPORT_CHAT_URL, +) -from config import CHANNEL_URL, CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, SUPPORT_CHAT_URL -from database import add_connection, add_referral, check_connection_exists, get_trial, use_trial +from database import ( + add_connection, + add_referral, + check_connection_exists, + get_trial, + use_trial, +) from handlers.keys.trial_key import create_trial_key from handlers.texts import INSTRUCTIONS_TRIAL, WELCOME_TEXT, get_about_vpn @@ -16,7 +34,9 @@ router = Router() @router.callback_query(F.data == "start") -async def handle_start_callback_query(callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool): +async def handle_start_callback_query( + callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool +): await start_command(callback_query.message, state, session, admin) @@ -37,7 +57,9 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin builder = InlineKeyboardBuilder() if trial_status == 0: - builder.row(InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn")) + builder.row( + InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn") + ) builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) builder.row( @@ -46,7 +68,9 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin ) if admin: - builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")) + builder.row( + InlineKeyboardButton(text="🔧 Администратор", callback_data="admin") + ) builder.row(InlineKeyboardButton(text="🌐 О нашем VPN", callback_data="about_vpn")) if os.path.isfile(image_path): @@ -98,16 +122,26 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session: Any): url=f'{CONNECT_ANDROID}{trial_key_info["key"]}', ), ) - builder.row(InlineKeyboardButton(text="💻 Windows/Linux", callback_data=f"connect_pc|{email}")) - builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")) + builder.row( + InlineKeyboardButton( + text="💻 Windows/Linux", callback_data=f"connect_pc|{email}" + ) + ) + builder.row( + InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile") + ) - await callback_query.message.answer(key_message, reply_markup=builder.as_markup()) + await callback_query.message.answer( + key_message, reply_markup=builder.as_markup() + ) @router.callback_query(F.data == "about_vpn") async def handle_about_vpn(callback_query: CallbackQuery): builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")) + builder.row( + InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate") + ) builder.row( InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL), ) @@ -116,4 +150,6 @@ async def handle_about_vpn(callback_query: CallbackQuery): ) builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="start")) - await callback_query.message.answer(get_about_vpn("3.2.21-Release"), reply_markup=builder.as_markup()) + await callback_query.message.answer( + get_about_vpn("3.2.21-Release"), reply_markup=builder.as_markup() + ) diff --git a/handlers/utils.py b/handlers/utils.py index 95871cf6..97ba02e9 100644 --- a/handlers/utils.py +++ b/handlers/utils.py @@ -1,11 +1,10 @@ import random import re -from typing import Optional import asyncpg +from config import DATABASE_URL from bot import bot -from config import DATABASE_URL from database import get_servers_from_db from logger import logger @@ -68,7 +67,9 @@ async def get_least_loaded_cluster() -> str: return least_loaded_cluster -async def handle_error(tg_id: int, callback_query: Optional[object] = None, message: str = "") -> None: +async def handle_error( + tg_id: int, callback_query: object | None = None, message: str = "" +) -> None: """ Обрабатывает ошибку, отправляя сообщение пользователю. @@ -80,7 +81,9 @@ async def handle_error(tg_id: int, callback_query: Optional[object] = None, mess try: if callback_query and hasattr(callback_query, "message"): 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 delete_error: logger.warning(f"Не удалось удалить сообщение: {delete_error}") diff --git a/logger.py b/logger.py index d12aace2..7130c5e9 100644 --- a/logger.py +++ b/logger.py @@ -1,7 +1,7 @@ -from datetime import timedelta import logging import os import sys +from datetime import timedelta from loguru import logger diff --git a/middlewares/admin.py b/middlewares/admin.py index 57aa0692..85819d32 100644 --- a/middlewares/admin.py +++ b/middlewares/admin.py @@ -1,24 +1,24 @@ -from typing import Any, Awaitable, Callable, Dict, Union +from collections.abc import Awaitable, Callable +from typing import Any from aiogram import BaseMiddleware from aiogram.types import TelegramObject - from config import ADMIN_ID class AdminMiddleware(BaseMiddleware): async def __call__( self, - handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], event: TelegramObject, - data: Dict[str, Any], + data: dict[str, Any], ) -> Any: data["admin"] = self._check_admin_access(event) return await handler(event, data) def _check_admin_access(self, event: TelegramObject) -> bool: try: - admin_ids: Union[int, list[int]] = ADMIN_ID + admin_ids: int | list[int] = ADMIN_ID if isinstance(admin_ids, list): return event.from_user.id in admin_ids return event.from_user.id == admin_ids diff --git a/middlewares/database.py b/middlewares/database.py index b1687aa6..c625fec3 100644 --- a/middlewares/database.py +++ b/middlewares/database.py @@ -1,18 +1,18 @@ -from typing import Any, Awaitable, Callable, Dict +from collections.abc import Awaitable, Callable +from typing import Any +import asyncpg from aiogram import BaseMiddleware from aiogram.types import TelegramObject -import asyncpg - from config import DATABASE_URL class DatabaseMiddleware(BaseMiddleware): async def __call__( self, - handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], event: TelegramObject, - data: Dict[str, Any], + data: dict[str, Any], ) -> Any: conn = await asyncpg.connect(DATABASE_URL) try: diff --git a/middlewares/delete.py b/middlewares/delete.py index 356f8831..7804b81c 100644 --- a/middlewares/delete.py +++ b/middlewares/delete.py @@ -1,4 +1,5 @@ -from typing import Any, Awaitable, Callable, Dict +from collections.abc import Awaitable, Callable +from typing import Any from aiogram import BaseMiddleware from aiogram.types import CallbackQuery, Message, TelegramObject @@ -7,15 +8,17 @@ from aiogram.types import CallbackQuery, Message, TelegramObject class DeleteMessageMiddleware(BaseMiddleware): async def __call__( self, - handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], event: TelegramObject, - data: Dict[str, Any], + data: dict[str, Any], ) -> Any: if isinstance(event, (Message, CallbackQuery)): if isinstance(event, Message): if not event.text.startswith("/start"): try: - await event.bot.delete_message(event.chat.id, event.message_id - 1) + await event.bot.delete_message( + event.chat.id, event.message_id - 1 + ) except Exception: pass await event.delete() diff --git a/middlewares/logging.py b/middlewares/logging.py index fbe31bae..1d0c1b38 100644 --- a/middlewares/logging.py +++ b/middlewares/logging.py @@ -1,4 +1,5 @@ -from typing import Any, Awaitable, Callable, Dict, Optional +from collections.abc import Awaitable, Callable +from typing import Any from aiogram import BaseMiddleware from aiogram.types import CallbackQuery, Message, TelegramObject @@ -9,9 +10,9 @@ from logger import logger class LoggingMiddleware(BaseMiddleware): async def __call__( self, - handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], event: TelegramObject, - data: Dict[str, Any], + data: dict[str, Any], ) -> Any: user_info = self._extract_user_info(event) @@ -23,7 +24,7 @@ class LoggingMiddleware(BaseMiddleware): ) return await handler(event, data) - def _extract_user_info(self, event: TelegramObject) -> Dict[str, Optional[str]]: + def _extract_user_info(self, event: TelegramObject) -> dict[str, str | None]: user_id = None username = None action = None diff --git a/middlewares/user.py b/middlewares/user.py index 969020ef..6d276e75 100644 --- a/middlewares/user.py +++ b/middlewares/user.py @@ -1,4 +1,5 @@ -from typing import Any, Awaitable, Callable, Dict +from collections.abc import Awaitable, Callable +from typing import Any from aiogram import BaseMiddleware from aiogram.types import TelegramObject, User @@ -9,9 +10,9 @@ from database import upsert_user class UserMiddleware(BaseMiddleware): async def __call__( self, - handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], + handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]], event: TelegramObject, - data: Dict[str, Any], + data: dict[str, Any], ) -> Any: if user := data.get("event_from_user"): await self._process_user(user) diff --git a/pyproject.toml b/pyproject.toml index fb4c1abc..1ea6e345 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,27 +1,17 @@ -[tool.black] -line-length = 120 # Совпадает с flake8 -target-version = ['py39','py310','py311'] # Укажите версию Python вашего проекта -skip-string-normalization = true # Отключает нормализацию кавычек -include = '\.pyi?$' # Включает Python-файлы -exclude = ''' -/( - \.git - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist -)/ -''' # Исключает системные папки +[tool.ruff] +line-length = 120 +target-version = "py310" -[tool.isort] -profile = "black" # Устанавливает совместимость с black -line_length = 120 -multi_line_output = 3 # Формат многострочных импортов -include_trailing_comma = true # Совместимость с black -force_sort_within_sections = true # Сортировка внутри секций -sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] -skip_gitignore = true # Учитывать .gitignore +[tool.ruff.lint] +select = ["E", "F", "W", "I", "N", "UP", "ANN", "ASYNC", "S", "BLE", "FBT", "B", "A", "C4", "DTZ", "T10", "ISC", "ICN", "G", "PIE"] +ignore = ["ANN101", "ANN102", "S101",'ANN201','ANN001','BLE001'] + +[tool.ruff.format] +quote-style = "single" +indent-style = "space" + +[tool.darker] +src = ["."] +revision = "HEAD" +diff = false +check = false \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 77ed916d..7a5d0a6f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,8 +30,4 @@ aiocryptopay py3xui sqlalchemy robokassa -flake8 -black -isort -pylint ping3 \ No newline at end of file diff --git a/servers.py b/servers.py index fbb81968..8dde3f5c 100644 --- a/servers.py +++ b/servers.py @@ -1,14 +1,14 @@ import asyncio -from datetime import datetime, timedelta import re +from datetime import datetime, timedelta +import asyncpg from aiogram.types import InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder -import asyncpg +from config import ADMIN_ID, DATABASE_URL from ping3 import ping from bot import bot -from config import ADMIN_ID, DATABASE_URL from database import get_servers_from_db from logger import logger @@ -16,7 +16,9 @@ try: from config import CLUSTERS except ImportError: CLUSTERS = None - logger.warning("Переменная CLUSTERS не найдена в конфигурации. Добавьте сервера через админ-панель!") + logger.warning( + "Переменная CLUSTERS не найдена в конфигурации. Добавьте сервера через админ-панель!" + ) async def sync_servers_with_db(): @@ -25,7 +27,9 @@ async def sync_servers_with_db(): Если CLUSTERS не найден, синхронизация не будет выполнена. """ if CLUSTERS is None: - logger.info("Конфигурация CLUSTERS не найдена. Синхронизация не будет выполнена.") + logger.info( + "Конфигурация CLUSTERS не найдена. Синхронизация не будет выполнена." + ) return try: @@ -55,14 +59,18 @@ async def sync_servers_with_db(): server_info["SUBSCRIPTION"], server_info["INBOUND_ID"], ) - logger.info(f"Сервер {server_info['name']} из кластера {cluster_name} добавлен в базу данных.") + logger.info( + f"Сервер {server_info['name']} из кластера {cluster_name} добавлен в базу данных." + ) else: - logger.info(f"Сервер {server_info['name']} из кластера {cluster_name} уже существует.") + logger.info( + f"Сервер {server_info['name']} из кластера {cluster_name} уже существует." + ) except Exception as e: logger.error(f"Ошибка при синхронизации серверов: {e}") finally: - if 'conn' in locals(): + if "conn" in locals(): await conn.close() @@ -96,14 +104,24 @@ async def notify_admin(server_name: str): current_time = datetime.now() last_notification_time = last_notification_times.get(server_name) - if last_notification_time and current_time - last_notification_time < timedelta(minutes=3): - logger.info(f"Не отправляем уведомление для сервера {server_name}, так как прошло менее 3 минут.") + if last_notification_time and current_time - last_notification_time < timedelta( + minutes=3 + ): + logger.info( + f"Не отправляем уведомление для сервера {server_name}, так как прошло менее 3 минут." + ) return - logger.info(f"Отправка уведомлений администратору о недоступности сервера {server_name}...") + logger.info( + f"Отправка уведомлений администратору о недоступности сервера {server_name}..." + ) builder = InlineKeyboardBuilder() - builder.row(InlineKeyboardButton(text="Управление сервером", callback_data=f"manage_server|{server_name}")) + builder.row( + InlineKeyboardButton( + text="Управление сервером", callback_data=f"manage_server|{server_name}" + ) + ) for admin_id in ADMIN_ID: await bot.send_message( @@ -116,7 +134,9 @@ async def notify_admin(server_name: str): parse_mode="HTML", reply_markup=builder.as_markup(), ) - logger.info(f"Уведомление отправлено администратору с ID {admin_id} о сервере {server_name}.") + logger.info( + f"Уведомление отправлено администратору с ID {admin_id} о сервере {server_name}." + ) last_notification_times[server_name] = current_time except Exception as e: @@ -140,7 +160,9 @@ async def check_servers(): server_name = server["server_name"] server_host = extract_host(original_api_url) - logger.debug(f"Проверка доступности сервера '{server_name}' с хостом {server_host}") + logger.debug( + f"Проверка доступности сервера '{server_name}' с хостом {server_host}" + ) is_online = await ping_server(server_host) @@ -148,12 +170,18 @@ async def check_servers(): last_ping_times[server_name] = current_time else: last_ping_time = last_ping_times.get(server_name) - if last_ping_time and current_time - last_ping_time > timedelta(minutes=3): - logger.warning(f"Сервер {server_name} не отвечает более 3 минут. Отправляю уведомление.") + if last_ping_time and current_time - last_ping_time > timedelta( + minutes=3 + ): + logger.warning( + f"Сервер {server_name} не отвечает более 3 минут. Отправляю уведомление." + ) await notify_admin(server_name) elif not last_ping_time: last_ping_times[server_name] = current_time - logger.info(f"Сервер {server_name} не отвечал ранее, но теперь зарегистрирован.") + logger.info( + f"Сервер {server_name} не отвечал ранее, но теперь зарегистрирован." + ) logger.info("Завершена проверка всех серверов.") await asyncio.sleep(30) From ce79caa58d980936c0320086d9755664de3404f2 Mon Sep 17 00:00:00 2001 From: Zakhar Izmaylov Date: Fri, 13 Dec 2024 12:13:50 +0300 Subject: [PATCH 2/2] Fix requirements.txt --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7a5d0a6f..72b819da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,4 +30,5 @@ aiocryptopay py3xui sqlalchemy robokassa -ping3 \ No newline at end of file +ping3 +ruff \ No newline at end of file